---
title: "Post an alert to an integration"
method: POST
path: "/api/v1/integrations/alerts/{integration}"
tags: ["Observability integrations"]
---

# Post an alert to an integration

`POST /api/v1/integrations/alerts/{integration}`

## Request

- Base URL: `https://wardenpoint.com` (the document declares no server; this is the origin it was published from)
- URL: `https://wardenpoint.com/api/v1/integrations/alerts/{integration}`
- Auth: HTTP bearer

The endpoint external monitoring tools call, and the one to call yourself to prove an integration you just created actually works end to end.

**Authentication is the integration secret, not an API key.** Send `Authorization: Bearer <secret>` or `X-WardenPoint-Secret: <secret>`; CloudWatch integrations may also use `?secret=<secret>` because AWS SNS cannot set headers. The secret is the one returned once by the create endpoint (or the newer one a dashboard rotation issued — after such a rotation the previous secret is still accepted for a grace window, so a sender that has not been updated yet keeps delivering). A request that fails to authenticate answers 401 and stores NOTHING: no alert, no event, nobody is paged. The rejection is recorded on the integration's `last_error` and in the `wardenpoint_observability_inbound_auth_failures_total` counter, so a sender left on a stale secret is visible rather than silent. An API key is neither needed nor accepted here — this route sits outside the key middleware entirely, and the tenant is taken from the integration in the path.

When the integration has `hmac_enabled: true`, the plain secret stops working. Signed requests need `X-WardenPoint-Timestamp` (current Unix seconds, within 300 s of server time) and `X-WardenPoint-Signature` (lowercase hex `hmac_sha256("<timestamp>.<raw body>", secret)`). Each signature is accepted exactly once inside the tolerance window, so a replayed request answers 401. GitHub integrations ignore the flag and always use GitHub's own `X-Hub-Signature-256` over the raw body, keyed by the same secret.

**Not idempotent, and deliberately so.** Repeating the same body does not overwrite anything: the alert is deduplicated by identity, so you get one alert with N events, and every one of those N counts toward the accumulation threshold that decides whether people get paged. A client that retries on timeout inflates the count. Send a stable `external_alert_id` (or `deduplication_key`) so repeats collapse onto one alert rather than creating several, and expect the event counter to grow.

**Unknown formats are accepted, not rejected.** Keys outside the whitelist are dropped and the rest is normalised as best it can be: an unrecognised body still produces an alert, titled `External alert`, severity `unknown`, status `firing`. That is what "the webhook worked but nothing sensible arrived" looks like — check `title` in the response's alert rather than assuming 202 means the mapping is right. The one shape that is refused outright is a malformed value in a recognised key: a `source_url` that is not a URL, or a `title` over 255 characters, answers 422.

**The severity you send does not set the notification priority.** It decides which routing rule matches; the matched rule's ACTION then decides how loudly the alert arrives. `escalate` produces a `critical` notification and starts the escalation chain. `message` produces a `normal` one and starts no chain at all — only the `initial` step of the escalation policy runs, so a policy that would have reached three people reaches one, and nothing waits for an acknowledgement. A `severity: warning` alert matched by an `escalate` rule wakes people; a `severity: critical` alert matched by a `message` rule wakes nobody. Read the `action` field of the response to see which of the two happened.

**202 does not mean somebody was paged.** Read `notification_queued`: when it is false, `queue_skip_reason` says why in machine-readable form — `suppressed_by_rule` (the accumulation policy is holding it back, see `event_count` vs `threshold_count`), `no_group` (no routing rule matched and the integration has no fallback group), `group_has_no_recipients`, `all_recipients_inactive` (the group has members but every one of them is switched off — their contacts are fine, the people are not), `channel_not_configured` (the people are switched on and their contacts are fine — the channel that would carry the alert is not set up on this installation, e.g. no mailer configured, so fix the setup rather than the contacts), `no_routable_contacts` (at least one member is switched on and still has no working contact for the configured channels) or `deferred_to_inbox_digest`. Verifying a fresh integration means checking that field, not the status code.\n\n**When the reason is about routing, read `queue_skip_detail` before touching anything.** `no_routable_contacts` and `channel_not_configured` name a symptom, and the repair depends on how the channel was chosen. The detail says which channel and provider the first step resolved to (`resolved_channel`, `resolved_provider`, the whole ladder in `resolved_steps`) and where that decision came from (`source`): `group_rule` / `group_policy` mean the group's own configuration picked it, `company_default_no_group_rule` means the group has no rule for this priority and the account-wide default was used, `company_default_empty_group_rule` means its rule carries no immediate step. `message` states all of it in one sentence. A null `resolved_channel` means no step resolved at all — the company can deliver on none of the channels of that priority, and nothing about the recipients or their contacts is wrong.\n\n**And `notification_queued: true` still does not promise a page.** A queued notification whose every channel fails ends `failed`, and at that point the umbrella guarantee fires: the account owner is emailed that the alert reached nobody. That email is not a page — the people this alert was routed to received nothing. It shows up as a notification of its own in `GET /notifications`, marked `is_owner_fallback: true`, while the alert's own notification keeps `delivered_via_owner_fallback: true` in its metadata and stays `failed`. When verifying an integration, read the notification's status through `GET /notifications/{notification}/status`; a rescue email in the account is evidence that delivery failed, never that it worked.

Two integration types short-circuit before an alert is created and answer 200 instead of 202: an AWS SNS `SubscriptionConfirmation` (which is confirmed for you) or `UnsubscribeConfirmation` on a CloudWatch integration, and a GitHub `ping` or a lifecycle event that is not a finished pass/fail run.

## Path parameters

- `integration` string, uuid, required

## Request body

- InboundAlertRequest: The body an external monitoring tool POSTs. Its shape depends on the integration's `type` — you send the tool's native webhook payload and WardenPoint normalises it. Every field is optional and unknown keys are ignored, so nothing here is a required contract; what the shape decides is whether the alert arrives with a usable title, severity, status and dedup identity, or as an unnamed blob. Whatever arrives is reduced to six canonical values: an identity used for deduplication, a title, a description, a severity (`critical`, `high`, `medium`, `low`, `info`, `unknown`), a status (`firing`, `resolved`, `acknowledged`, `suppressed`) and a source URL. Severity and status are normalised from common vocabularies — `fatal`/`panic`/`emergency` read as critical, `error`/`err`/`alert` as high, `warning`/`warn` as medium, `notice` as low, `debug` as info; `closed`/`ok`/`recovered` read as resolved, `ack`/`silenced` as acknowledged, `muted` as suppressed. Anything unrecognised becomes severity `unknown` and status `firing`. Per type: * `generic_webhook` — the flat object documented by the properties below. This is the shape to send when you control the sender. With `field_mapping` configured on the integration, the raw body is read instead and the mapped dot-paths win, so an arbitrary JSON document works without matching these names. * `grafana` — Grafana's unified-alerting webhook. Read from `alerts[0]` when present, else from the top level: `fingerprint` is the identity, `annotations.summary` the title, `annotations.description` the body, `labels.severity` the severity, `status` the status, `generatorURL`/`externalURL` the source link. * `prometheus_alertmanager` — the Alertmanager v4 webhook envelope, modelled as a **group**: `groupKey` is the identity (so a whole group is one alert), the top-level `status` decides firing/resolved, and `commonAnnotations` / `commonLabels` / `groupLabels` supply title and severity. Members arrive in `alerts[]` and are summarised into the description when the group has no common one. * `zabbix` — the body produced by WardenPoint's Zabbix webhook media type: `event_id` (identity, stable across problem→recovery), `event_name`, `severity` as the raw Zabbix word (`Disaster`, `High`, `Average`, `Warning`, `Information`, `Not classified` — mapped server-side), `status` as `PROBLEM`/`RESOLVED` or `event_value` as 1/0, plus `host`, `host_groups`, `tags`, `operational_data`, `item_name`, `item_value`, `trigger_url`. * `datadog`, `cloudwatch`, `uptime_kuma`, `sentry`, `kibana`, `github` — those tools' own webhook bodies, unmodified. CloudWatch arrives inside the AWS SNS envelope (`Type`, `Message`, `SubscribeURL`); GitHub carries the event name in the `X-GitHub-Event` header; GitLab sends its secret token in `X-Gitlab-Token` and the hook type in `X-Gitlab-Event` (only `Merge Request Hook` is ingested). * `email` — has no webhook at all. This endpoint answers 404 for email integrations; they receive mail at `inbound_email_address`.
  - `external_alert_id` string: Identity of the alert in the source system. Two POSTs carrying the same one land on the same WardenPoint alert instead of creating a second. `alert_id`, `fingerprint` and `id` are accepted as synonyms, in that order.
  - `deduplication_key` string: Explicit override of the identity. When absent, `external_alert_id` is used; when that is absent too, a hash of integration + title + severity + labels stands in — which means a title that changes between occurrences silently starts a new alert. Send a stable id if you want reliable deduplication.
  - `title` string: One-line summary. Falls back to `message`, then `annotations.summary`, then `annotations.title`, then the literal `External alert` — seeing that string in the dashboard means nothing in the body was recognised as a title.
  - `description` string: Longer body text. Falls back to `annotations.description`.
  - `severity` string: Source severity in the source's own words; normalised on arrival. Falls back to `labels.severity`, then `level`. Unrecognised values become `unknown`, which routing rules restricted to real severities will not match.
  - `status` string: Lifecycle state. Anything unrecognised — including an omitted field — is read as `firing`, so a recovery must say so explicitly (`resolved`, `ok`, `closed`, `recovered`) or it pages people again. `state` is accepted as a synonym.
  - `labels` object: Key/value dimensions. Routing rules match on these, so send the ones you route by (`service`, `env`, `cluster`).
  - `annotations` object: Free-form extras. `summary` and `description` are read as title/body fallbacks.
  - `source_url` string, uri: Link back to the alert in the source system, shown to the person who gets paged. `url` and `web_url` are accepted as synonyms. Must be a valid URL or the whole request is rejected with 422.

## Response `200`

A control message was handled and no alert was created: an SNS subscription confirmed or unsubscribe acknowledged (CloudWatch), or a GitHub `ping` or non-actionable lifecycle event. Body carries `success` and a human-readable `message`; GitHub ignores also set `ignored: true`.

## Other responses

- `202`: Alert recorded. Whether anybody was notified is in `notification_queued`.
- `401`: The integration secret is missing or wrong — or, on an HMAC integration, the timestamp is outside the 300-second tolerance, the signature does not match the raw body, or the same signature was already used once.
- `403`: The integration exists but `is_active` is false. Nothing is ingested while it is switched off; flip it back with the update endpoint.
- `404`: No integration with this UUID, or it is an `email` integration — those receive mail at `inbound_email_address` and have no webhook.
- `413`: Raw body above the ingest cap (1 MiB by default). Nothing is parsed or stored; trim the payload at the source.
- `422`: A recognised key carried an unusable value — a `source_url` that is not a URL, a `title` longer than 255 characters, a non-integer `truncatedAlerts`. Unrecognised keys are dropped rather than rejected, so a 422 always points at a whitelisted field.
- `429`: More than 60 alerts a minute from one IP to this endpoint. Alert sources that legitimately burst harder should batch on their side; the counter is per IP, not per integration.

---

[API](https://skmtc.dev/wardenpoint/apis/wardenpoint-api.md) · [All operations](https://skmtc.dev/wardenpoint/apis/wardenpoint-api/llms.txt) · [OpenAPI document](https://skmtc.dev/wardenpoint/apis/wardenpoint-api/revisions/259ee445728e?raw)
