> ## Documentation Index
> Fetch the complete documentation index at: https://docs.use.observer/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive alerts from your monitoring

> Send alerts from Prometheus Alertmanager, Grafana Alerting, Datadog, AWS CloudWatch or any JSON source to Observer. Mapping rules turn them into alert-backed metrics that drive status pages, incidents and SLOs.

If you already alert on your systems, Observer can use those alerts
directly. You add an **inbound alert source**, point your alerting tool at
its URL, and write mapping rules that send matching alerts to
**alert-backed metrics**. An alert-backed metric is unhealthy or degraded
while a routed alert fires, and healthy when none do. Status pages,
incidents, SLOs and notifications treat it like any other metric.

No agent is involved: senders deliver alerts to Observer over HTTPS.

<Info>
  **Plans**

  Inbound alert sources are available from the Starter plan. Starter
  includes 3 sources, Pro 10, Business 25, and Enterprise has no limit. Only
  enabled sources count.
</Info>

## How it works

1. Your alerting tool sends a webhook to the source's intake URL with the
   source's secret.
2. Observer parses the alerts and keeps the current state of each one
   (firing or resolved).
3. Each firing alert is checked against the source's mapping rules, in
   order. The first rule that matches decides which metric it drives and
   with which status.
4. Each alert-backed metric takes the worst status among the alerts routed
   to it. With none firing, it is healthy.

## Add a source

<Steps>
  <Step title="Create the source">
    In the console, open **Alerts** and find the **Inbound** section. Add a
    source, pick the sender (Prometheus Alertmanager, Grafana Alerting,
    Datadog, AWS CloudWatch, or Generic JSON), and name it.

    For Grafana and Generic JSON you can also turn on request signing. See
    [Signed requests](#signed-requests).
  </Step>

  <Step title="Copy the URL and the secret">
    Observer shows the intake URL and the secret. The secret (and the signing
    secret, if you turned signing on) is shown **once**. Copy it now. Observer
    stores it hashed or encrypted and cannot show it again. If it leaks, rotate
    it from the source page; the old secret stops working immediately.
  </Step>

  <Step title="Configure the sender">
    The source page shows a setup snippet for the sender you picked, with the
    URL filled in. The sections below show each one.
  </Step>

  <Step title="Add mapping rules">
    Under **Mapping rules**, add a rule, set its conditions, and choose the
    metric it drives. Pick an existing alert-backed metric or create one from
    the rule with **New alert-backed metric**. See
    [Mapping rules](#mapping-rules).
  </Step>

  <Step title="Send a test event">
    Send a test event from the sender (see
    [Send a test event](#send-a-test-event)). It arms the source, and the
    source page lists it under **Deliveries** with the outcome **Test**. Real
    alerts appear under **Alerts** as they arrive.
  </Step>
</Steps>

## Set up each sender

The intake URL has the form `https://use.observer/api/intake/<source_id>`.
The id in the URL only locates the source and authorizes nothing on its
own. Every request must also carry the secret.

<Tabs>
  <Tab title="Alertmanager">
    Add a webhook receiver to `alertmanager.yml` and route the alerts you want
    to it. Observer reads webhook payload version 4.

    ```yaml alertmanager.yml theme={null}
    receivers:
      - name: observer
        webhook_configs:
          - url: https://use.observer/api/intake/<source_id>
            send_resolved: true
            http_config:
              authorization:
                type: Bearer
                credentials: <your intake secret>

    route:
      receiver: observer   # or add it as a child route with matchers
    ```

    Keep `send_resolved: true` so recoveries reach Observer. Labels become the
    alert's labels, and `severity` (or `priority`) becomes its severity. When
    an alert has no fingerprint, Observer derives one from its labels.
  </Tab>

  <Tab title="Grafana Alerting">
    Create a **Webhook** contact point and attach it to a notification policy.

    ```text theme={null}
    Integration:               Webhook
    URL:                       https://use.observer/api/intake/<source_id>
    HTTP method:               POST
    Authorization scheme:      Bearer
    Authorization credentials: <your intake secret>
    ```

    Leave **Disable resolved message** off so recoveries reach Observer.
    Observer also accepts the payloads of legacy (pre unified alerting)
    Grafana alert rules, where `alerting` is firing and `ok` is resolved.
  </Tab>

  <Tab title="Datadog">
    In Datadog's **Webhooks** integration, add a webhook:

    ```text theme={null}
    Name:           observer
    URL:            https://use.observer/api/intake/<source_id>
    Custom headers: {"Authorization": "Bearer <your intake secret>"}
    ```

    Use this payload:

    ```json theme={null}
    {
      "id": "$ID",
      "alert_id": "$ALERT_ID",
      "aggreg_key": "$AGGREG_KEY",
      "title": "$EVENT_TITLE",
      "transition": "$ALERT_TRANSITION",
      "alert_type": "$ALERT_TYPE",
      "priority": "$ALERT_PRIORITY",
      "scope": "$ALERT_SCOPE",
      "hostname": "$HOSTNAME",
      "tags": "$TAGS",
      "date": "$DATE",
      "link": "$LINK"
    }
    ```

    Then mention `@webhook-observer` in each monitor's message. Keep recovery
    notifications on.

    Observer reads the transition: **Triggered** fires with severity
    `critical`, **Warn** fires with severity `warning`, **No Data** fires with
    severity `no_data`, and the matching **Recovered** transitions resolve.
    Tags in `key:value` form become labels. Each monitor and scope pair is one
    alert, and Datadog's `$ID` is used to ignore retried deliveries.
  </Tab>

  <Tab title="CloudWatch (SNS)">
    CloudWatch alarms reach Observer through an Amazon SNS topic. SNS cannot
    set headers, so the secret goes in the subscription URL as the basic auth
    password. The console shows the full command with the URL filled in:

    ```bash theme={null}
    aws sns subscribe \
      --topic-arn arn:aws:sns:<region>:<account-id>:<topic> \
      --protocol https \
      --notification-endpoint 'https://observer:<your intake secret>@use.observer/api/intake/<source_id>'
    ```

    Point each alarm's **ALARM** and **OK** actions at the topic.

    Observer confirms the subscription itself. The first confirmed topic is
    pinned to the source, and messages from any other topic are refused. You
    can unpin it from the source page; the next confirmed subscription is
    pinned instead.

    `ALARM` is firing and `OK` is resolved. `INSUFFICIENT_DATA` is ignored, so
    the alert keeps its last state. The alarm name, account, region, namespace,
    metric name and each dimension (as `dimension.<name>`) become labels.
    CloudWatch alarms carry no severity.
  </Tab>

  <Tab title="Generic JSON">
    Any tool or script that can send an HTTP POST can use the generic format:

    ```bash theme={null}
    curl -X POST 'https://use.observer/api/intake/<source_id>' \
      -H 'Authorization: Bearer <your intake secret>' \
      -H 'Content-Type: application/json' \
      -d '{
        "event_id": "evt-2026-10-14-0001",
        "alerts": [
          {
            "fingerprint": "checkout-api-5xx",
            "status": "firing",
            "title": "Checkout API error rate above 2%",
            "severity": "critical",
            "labels": { "service": "checkout", "env": "prod" },
            "starts_at": "2026-10-14T09:12:00Z",
            "url": "https://grafana.example.com/d/abc"
          }
        ]
      }'
    ```
  </Tab>
</Tabs>

### Generic JSON schema

The body is an object with an `alerts` array, or a single alert object on
its own.

| Field | Type | Required | Notes |
| - | - | - | - |
| `event_id` | string | no | Up to 256 characters. A delivery with an `event_id` the source has already accepted is acknowledged and not applied again. |
| `alerts` | array | yes | 1 to 500 alerts. |
| `alerts[].fingerprint` | string | yes | Up to 256 characters. Identifies the alert across firing and resolved messages. |
| `alerts[].status` | string | yes | `firing` or `resolved`. |
| `alerts[].title` | string | no | Shown in the console. Defaults to the fingerprint. |
| `alerts[].severity` | string | no | Used by rules that map status by severity. |
| `alerts[].labels` | object | no | String, number or boolean values. Used by rule conditions. |
| `alerts[].starts_at` | string | no | ISO 8601 time the alert started. |
| `alerts[].ends_at` | string | no | ISO 8601 time the alert ended. |
| `alerts[].url` | string | no | A link back to the alert in your tool. |

## Arming

A source is **armed** by its first verified event: a request that passed
the secret check (and the signature check, when signing is on) and could
be parsed. A test event or a real alert both arm it. Requests with a wrong
secret, a bad signature or a body that cannot be parsed never arm a
source.

* **Before it is armed**, the metrics the source drives read **No data**.
  Observer has no evidence yet that the sender can reach it, so it does
  not assume healthy.
* **Once it is armed**, a metric with no firing alerts routed to it reads
  **healthy**.

The source page shows the arming state under **Arming**, with the test
event instructions for its sender. The sources list shows **Not armed**
for a source that has not received a verified event yet.

### Send a test event

A test event arms the source and is recorded under **Deliveries** with the
outcome **Test**. It is never routed to a metric and never changes a
status.

For every sender except CloudWatch, a request with the header
`X-Observer-Test: 1` is a test event. The header is checked after the
secret and the signature, so a test event proves the sender's
credentials.

<Tabs>
  <Tab title="Alertmanager">
    Alertmanager has no test button. Run this from a host that can reach
    Observer, or wait for the first real notification. Either one arms the
    source. With a [heartbeat](#heartbeat-alert) configured, the first
    Watchdog notification arms it too.

    ```bash theme={null}
    curl -X POST 'https://use.observer/api/intake/<source_id>' \
      -H 'Authorization: Bearer <your intake secret>' \
      -H 'X-Observer-Test: 1'
    ```
  </Tab>

  <Tab title="Grafana Alerting">
    In Grafana, open **Alerting**, then **Contact points**, open the Observer
    contact point, and use **Test**, then **Send test notification**. Grafana's
    test alert (`alertname` `TestAlert`) arms the source and is never routed to
    a metric.

    Without request signing, you can also send the curl request shown for
    Alertmanager.
  </Tab>

  <Tab title="Datadog">
    Datadog's monitor test notifications look like real alerts, so they are
    not treated as test events. Send this instead, or wait for the first real
    notification:

    ```bash theme={null}
    curl -X POST 'https://use.observer/api/intake/<source_id>' \
      -H 'Authorization: Bearer <your intake secret>' \
      -H 'X-Observer-Test: 1'
    ```
  </Tab>

  <Tab title="CloudWatch (SNS)">
    After the subscription is confirmed, publish a test message to the topic.
    SNS signs it, so it proves the whole path:

    ```bash theme={null}
    aws sns publish \
      --topic-arn arn:aws:sns:<region>:<account-id>:<topic> \
      --message '{"observer_test": true}'
    ```
  </Tab>

  <Tab title="Generic JSON">
    Send the body `{"test": true}`. If signing is on, sign it like any other
    request.

    ```bash theme={null}
    curl -X POST 'https://use.observer/api/intake/<source_id>' \
      -H 'Authorization: Bearer <your intake secret>' \
      -H 'Content-Type: application/json' \
      -d '{"test": true}'
    ```
  </Tab>
</Tabs>

## Heartbeat alert

Without a heartbeat, a source that goes silent looks the same as a
source with nothing firing: its metrics read healthy. To tell the two
apart, configure an optional **heartbeat alert** on the source: an alert
your sender keeps firing at all times, such as Alertmanager's `Watchdog`.

Under **Heartbeat** on the source page, turn it on and set:

* **Heartbeat alert**: one or more conditions that identify it, written
  like [mapping rule](#mapping-rules) conditions. The default is
  `alertname` `=` `Watchdog`.
* **No data after**: how many minutes without it before the source's
  metrics read No data. The default is 10 minutes, and it can be set from
  2 to 1440.

How it behaves:

* The heartbeat is never routed as an alert. It never drives a metric,
  and a firing heartbeat only records that the sender is alive.
* When no heartbeat has arrived within the window, the metrics the
  source's enabled rules drive read **No data**, with the reason
  "Heartbeat alert missing". This holds even if other alerts still
  arrive.
* When the heartbeat resumes, the metrics return to the status their
  alerts give them.
* Saving the heartbeat settings restarts the window, so a new heartbeat
  gets one full window to arrive. Turning the heartbeat off returns the
  metrics to the status their alerts give them.

These changes follow the metric's dwell, like any other status change,
and notify as usual. The sources list shows **Heartbeat missing** for a
source whose heartbeat has stopped.

## Authentication

Every request must carry the source's secret (it starts with `obs_in_`).
Observer accepts it from, in order:

* `Authorization: Bearer <secret>`
* `Authorization: Basic ...` with the secret as the password (any user
  name)
* `X-Observer-Token: <secret>`, for senders that cannot set
  `Authorization`

The secret is never read from the query string. An unknown source and a
wrong secret get the same `401`. Observer stores the secret hashed and
never stores the body of a request that failed authentication.

### Signed requests

For Grafana and Generic JSON sources you can also require an HMAC-SHA256
signature over the raw request body. Unsigned requests, wrong signatures
and timestamps more than 5 minutes from now are refused.

* **Grafana**: set the contact point's HMAC signature secret to the
  source's signing secret. Grafana sends `X-Grafana-Alerting-Signature`.
  If you also configure the timestamp header
  `X-Grafana-Alerting-Signature-Timestamp`, the signature covers
  `<timestamp>:<body>`.
* **Generic JSON**: send `X-Observer-Timestamp` (Unix seconds or
  milliseconds) and
  `X-Observer-Signature: sha256=<hex(HMAC-SHA256(signing_secret, timestamp + "." + body))>`.
  This is the same format Observer uses to sign its own
  [outbound webhooks](/docs/guides/outbound-webhooks).

### CloudWatch signatures

For CloudWatch sources, Observer verifies the AWS signature on every SNS
message (signature versions 1 and 2) and only fetches signing
certificates from Amazon SNS hosts. The secret in the subscription URL is
still required.

## Mapping rules

Rules are evaluated in order, and the first enabled rule whose conditions
all match wins. A rule with no conditions matches every alert. Alerts that
match no rule are kept on the source page but drive nothing.

Conditions are Prometheus style and test one label each:

| Operator | Meaning |
| - | - |
| `=` | equals |
| `!=` | does not equal |
| `=~` | matches regex |
| `!~` | does not match regex |
| `exists` | the label is set |
| `!exists` | the label is not set |

Regexes match the whole value, as in Alertmanager. Patterns longer than
200 characters, nested repeats such as `(a+)+`, repeated alternations such
as `(a|aa)*`, backreferences and lookarounds are refused.

Each rule sets the status its alerts drive **while firing**:

* **Unhealthy** or **Degraded**: every matching alert drives that status.
* **By severity**: the alert's severity decides. You can set overrides on
  the rule. Without an override, these defaults apply:

| Severity | Status |
| - | - |
| `critical`, `crit`, `error`, `high`, `major`, `page`, `emergency`, `alert`, `p1`, `p2` | Unhealthy |
| `warning`, `warn`, `medium`, `minor`, `low`, `info`, `p3`, `p4`, `p5`, `no_data` | Degraded |
| Any other value, or no severity | Unhealthy |

An alert whose severity Observer does not recognize is treated as
unhealthy, so it never reads healthier than it is.

Editing rules re-routes the source's alerts that are firing at that time.

## Firing and resolved

* An alert-backed metric is **unhealthy** if any routed alert maps to
  unhealthy, **degraded** if the worst maps to degraded, and **healthy**
  when no routed alert is firing.
* Its value is a severity score: `0` healthy, `1` degraded, `2` unhealthy.
  The thresholds are fixed to match, so the value and the status always
  agree.
* Dwell starts at 0, because senders already debounce (for example
  Alertmanager's `for:`). You can raise it on the metric. Status changes
  then follow the same dwell, incident, SLO and notification rules as any
  other metric. See [Thresholds and dwell](/docs/concepts/thresholds-and-dwell).
* Late messages are ignored: a delayed firing for an alert that has since
  resolved, or a resolve for an older firing, does not change the state.
* A new alert-backed metric reads **No data** until its source is
  [armed](#arming). It never starts out healthy by assumption.

### Auto-resolve

Auto-resolve is off by default. With it off, a firing alert stays firing
in Observer until the sender sends a resolved message for it, however
long that takes. Observer never resolves an alert on its own.

Most senders send a resolved message when an alert clears. If yours does
not (for example Alertmanager with `send_resolved: false`, Grafana with
**Disable resolved message** on, or a script that only reports firing), or
if a resolved message is lost, the metric stays unhealthy or degraded
until you resolve it from the sender. For such senders, set
**Auto-resolve** on the source (from 5 minutes to 7 days): a firing alert
the sender has not repeated within that time is resolved.

## Responses

| Status | `error` | Meaning |
| - | - | - |
| `200` | | Accepted. Duplicate deliveries are acknowledged with `"duplicate": true` and not applied again. |
| `400` | `invalid_json`, `invalid_payload` | The body could not be parsed for this sender. `detail` says why. |
| `401` | `unauthorized`, `invalid_signature` | Missing or wrong secret, or a bad or stale signature. |
| `403` | `integration_disabled` | The source is paused. |
| `403` | `plan_required` | The organization's plan no longer includes inbound alerts. |
| `403` | `invalid_signature` | CloudWatch: the SNS message signature could not be verified. |
| `403` | `topic_not_allowed` | CloudWatch: the SNS topic is not the pinned one. |
| `405` | `method_not_allowed` | Use `POST`. |
| `413` | `payload_too_large` | The body is larger than 512 KB. |
| `429` | `rate_limited` | More than 120 requests per minute for this source. Retry after the `Retry-After` header. |

## Raw payloads and retention

The source page lists each authenticated delivery under **Deliveries**,
and you can open one to see its raw payload. Raw payloads are kept for
**90 days**. Resolved alerts are removed 90 days after they resolve.

Pausing a source makes it answer `403` and change nothing. Deleting a
source stops its URL immediately and removes its rules, alerts and
delivery log. Metrics it drove recover.

Every change to a source, its rules or its secrets is recorded in the
[audit log](/docs/reference/audit-log-events).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.