# Alert API and generic webhook

If your tool is not in the catalog, or you want to raise alerts from your own code, use the **API** integration. It accepts a small JSON body in which only a summary is required, and it can also acknowledge and resolve alerts. Its body is compatible with the Events v2 shape used by other paging tools, so a tool that already speaks it only needs a new URL and key.

## Set up the API integration

:::steps
### Create the integration
Go to **On-Call → Integrations → + Add Integration**, choose **API**, choose an **Escalation Policy** and click **Create Integration**.
### Copy the URL and key
Open the integration. The **Webhook URL** is `https://evohub.io/ingest/api?key=<key>`; the **API Key** is the key on its own.
### Send a test event
Run the `curl` example below with your key and check that an alert appears in **On-Call → Alerts**.
:::

## Send an event

`POST https://evohub.io/ingest/api` with a JSON body. Send the integration key in whichever way your tool makes easiest:

- `"routing_key"` in the body,
- `?key=` in the URL, or
- an `Authorization: Bearer <key>` header.

:::code-group
```bash [Minimal trigger]
curl -X POST 'https://evohub.io/ingest/api' \
  -H 'Authorization: Bearer YOUR_INTEGRATION_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"summary": "Database primary is down"}'
```

```bash [Full trigger]
curl -X POST 'https://evohub.io/ingest/api' \
  -H 'Content-Type: application/json' \
  -d '{
    "routing_key": "YOUR_INTEGRATION_KEY",
    "event_action": "trigger",
    "dedup_key": "db-primary-down",
    "payload": {
      "summary": "Database primary is down",
      "source": "db-01.prod.acme.example",
      "severity": "critical",
      "component": "postgres",
      "group": "payments",
      "custom_details": { "region": "eu-central" }
    }
  }'
```

```bash [Resolve]
curl -X POST 'https://evohub.io/ingest/api' \
  -H 'Content-Type: application/json' \
  -d '{
    "routing_key": "YOUR_INTEGRATION_KEY",
    "event_action": "resolve",
    "dedup_key": "db-primary-down"
  }'
```
:::

### Request fields

You can send the alert details nested in `payload` (Events v2 style) or as flat fields at the top level. A flat field fills the matching `payload` field when that one is empty.

| Field | Required | Description |
| --- | --- | --- |
| `routing_key` | No* | The integration key. *Required unless sent as `?key=` or a bearer token. |
| `event_action` | No | `trigger` (default), `acknowledge` or `resolve`. |
| `dedup_key` | For acknowledge and resolve | Identifies the alert. On a trigger without one, EvoHub derives a key from the summary and source and returns it. |
| `payload.summary` / `summary` / `title` | For trigger | The alert's title. |
| `payload.severity` / `severity` | No | `critical`, `high`, `medium`, `low` or `info`. The Events v2 values are accepted too: `error` is read as `high` and `warning` as `medium`. Anything else, or nothing, is `medium`. |
| `payload.source` / `source` | No | Where the problem is, for example a host name. Stored as the `source` label. |
| `description` / `payload.description` | No | The alert's description. |
| `payload.component`, `payload.group`, `payload.class` | No | Stored as labels of the same name. |
| `payload.custom_details` | No | An object; each entry becomes a label, its value as text. |
| `payload.timestamp`, `client`, `client_url` | No | Accepted for compatibility and ignored. The alert is stamped when it arrives. |

### Response

A processed event returns `202`:

```json
{
  "data": {
    "status": "success",
    "message": "Event processed",
    "dedup_key": "db-primary-down"
  },
  "success": true
}
```

Keep the `dedup_key` if you let EvoHub derive it — you need it to resolve the alert later.

A request that was not processed returns the same envelope with `"status": "invalid event"` and a `message`:

| Status | When | Example `message` |
| --- | --- | --- |
| `400` | The body is not JSON, a trigger has no summary, an acknowledge or resolve has no `dedup_key`, or `event_action` is unknown. | `summary is required` |
| `401` | No key was sent, or it names no enabled integration. | `invalid routing_key` |
| `500` | The alert could not be created, acknowledged or resolved. | `failed to create alert` |
| `503` | The key could not be checked right now. Retry after the `Retry-After` seconds. | `could not verify routing_key right now; retry later` |

### How events behave

- **Trigger** opens an alert and starts the integration's escalation policy. A trigger whose `dedup_key` matches an alert that is still open does not open a second one; it is recorded as **Retriggered** on the existing alert.
- **Acknowledge** acknowledges the open, triggered alert with that `dedup_key`.
- **Resolve** resolves the open alert with that `dedup_key`.
- Acknowledging or resolving a `dedup_key` that has no open alert does nothing and still returns `202`.
- The integration's escalation policy always applies; the request cannot choose another one.

## Generic alert webhook

EvoHub also accepts a simpler, flat alert format at `/ingest/alerts`. Use it with the key of your API integration:

```bash
curl -X POST 'https://evohub.io/ingest/alerts?key=YOUR_INTEGRATION_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Disk almost full on web-03",
    "description": "/var is at 93%",
    "severity": "high",
    "source": "cron-disk-check",
    "fingerprint": "web-03-disk-var",
    "labels": { "host": "web-03.acme.example" },
    "annotations": { "runbook": "https://wiki.acme.example/disk" }
  }'
```

| Field | Required | Description |
| --- | --- | --- |
| `title` | Yes | The alert's title. |
| `description` | No | The alert's description. |
| `severity` | No | `critical`, `high`, `medium` (default), `low` or `info`. |
| `source` | No | Shown as the alert's source. Defaults to `webhook`. |
| `fingerprint` | No | Deduplicates repeated sends while the alert is open. |
| `labels`, `annotations` | No | String-to-string maps, shown on the alert and usable in the voice template. |
| `escalation_policy_id` | No | Used only when the integration has no escalation policy of its own. |

The key goes in `?key=` only. A missing `title` returns `400` with the code `VALIDATION_FAILED` and the field in `details`; a bad key returns `401` with `INVALID_KEY`. A successful call returns `200` with `{"data": {"received": 1, "created": 1, "resolved": 0}, "success": true}`. This format cannot acknowledge or resolve; use the API events above for that.

## Tips

- Choose a stable `dedup_key` (or `fingerprint`) per problem, such as `<host>-<check>`, so repeats while the problem lasts do not open new alerts and your resolve finds the right alert.
- Send a resolve when your check passes again. Without one, the alert stays open until someone resolves it.
- Only new alerts count toward usage; retriggers of an open alert do not. See [How billing works](https://docs-dev.evohub.io/how-billing-works.md).

## Related

- [Integrations overview](https://docs-dev.evohub.io/integrations-overview.md)
- [Alerts and incidents](https://docs-dev.evohub.io/alerts-and-incidents.md)
- [Escalation policies](https://docs-dev.evohub.io/escalation-policies.md)
