Sending alerts
One POST opens an alert and pages whoever is on call.
Two ways in
Both accept the same thing — a signal aimed at a service. They differ only in where the credential goes.
| Lane | Auth | Use it when |
|---|---|---|
POST /v1/alerts | Authorization: Bearer ak_… | You control the sender and can set a header. |
POST /v1/ingest/{token} | Token in the URL (ik_…) | The sender can only be given a URL — most vendor webhook configs. |
An ingest URL is a credential: anyone holding it can raise alerts in your organization. Treat it like a password, and rotate it from the integration's page if it leaks.
Your first alert
Create a service in the dashboard. Every new service ships with a default integration and an ingest URL, so there is nothing else to set up.
curl -X POST https://api.acked.dev/v1/alerts \
-H "Authorization: Bearer ak_YOUR_KEY" \
-H "content-type: application/json" \
-d '{
"service_id": "svc_01hexample",
"event_key": "disk-full:db-1",
"title": "Disk 91% full on db-1",
"body": "/var is filling. Growth is ~2%/hour.",
"priority": "high"
}'
The response carries the alert id:
{"ok":true,"alert_id":"01KX…","deduped":false,"resolved":false}
Fields
| Field | Required | Notes |
|---|---|---|
service_id | always | Which service this is about. Decides who gets paged. |
title | to trigger | What someone reads at 3am. Keep it specific. |
event_key | to resolve | Your identifier for the condition. See deduplication. |
body | no | Detail. Shown on the alert page. |
priority | no | critical, high, medium, low, info. Defaults to high. |
urgency | no | high or low — see below. |
source | no | Free text naming the system that sent it. |
action | no | trigger (default) or resolve. |
Priority is not urgency
Priority is a label describing how bad the condition is. It does not decide how loudly anyone is notified.
Urgency decides the notification. high pages as a critical alert, which can break through a phone's silent switch and Focus modes where the device allows it. low arrives as an ordinary notification.
They are separate on purpose: a critical-priority condition during a planned migration may not warrant waking anyone, and a low-priority one at 4am might. If you omit urgency, anything that is not info pages at high urgency.
Deduplication
Send the same event_key for the same service while an alert is still open, and it folds into that alert instead of opening a second one. The dedupe count goes up; nobody is paged again.
This is what makes it safe to post on every check. A monitor firing every 30 seconds produces one alert, not 120 an hour.
Choose an event_key that identifies the condition, not the moment — disk-full:db-1, not disk-full:db-1:2026-08-03T18:04Z. Omit it entirely and every call opens a new alert.
Resolving
When the condition clears, send the same key with action: "resolve". No title needed — you are closing something that already exists.
curl -X POST https://api.acked.dev/v1/alerts \
-H "Authorization: Bearer ak_YOUR_KEY" \
-H "content-type: application/json" \
-d '{"service_id":"svc_01hexample","event_key":"disk-full:db-1","action":"resolve"}'
Resolving something already closed is a no-op, not an error, so a recovery hook that fires twice is harmless.
What a 202 means
Ingest answers 202 Accepted when the alert has been taken but not yet written — the edge buffers it and delivers it onward. You will see {"ok":true,"alert_id":"…","buffered":true}.
This is deliberate. A page must survive us having a bad moment, so the edge accepts and holds rather than failing your monitor's webhook. The alert id is yours either way, and the alert will open.
The practical consequence: do not read the response to decide whether an alert exists. It does.