API Reference

One endpoint, one POST request. Send JSON to your channel's webhook URL and we handle notifications, formatting, and delivery.

Send Alert

Trigger a new alert in a specific channel. This is the primary endpoint for sending notifications from your application.

POSThttps://zenhook.dev/api/webhook/{token}
Example Request
curl -X POST https://zenhook.dev/api/webhook/abc123def456 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "New signup",
    "level": "success",
    "emoji": "🎉",
    "source": "My App",
    "actor": {
      "id": "42",
      "name": "Jane Doe",
      "email": "[email protected]",
      "plan": "Pro"
    },
    "linkUrl": "https://myapp.com/admin/users/42",
    "linkText": "View Profile"
  }'

Path Parameters

tokenstringRequired
Your unique channel webhook token. Find it in channel settings.

Request Body

titlestringRequired
A brief summary of the alert. Used as the headline in notifications. Max 200 characters.
levelenum
Severity of the alert: info, success, warning, error. Defaults to info.
messagestring
Detailed body text. Max 5,000 characters.
emojistring
Custom emoji displayed next to the title. Accepts a unicode character ("🚀"), a name ("rocket"), or a shortcode (":rocket:"). Names and shortcodes are translated on ingest. Unknown names are dropped rather than rendered as literal text. Known names: rocket · siren · warning · check · cross · fire · bug · zap · bell · tada · sparkles · envelope · chat · phone · megaphone · wave · eyes · heart · thumbsup · office · door · house · factory · lock · unlock · key · link · package · gear · wrench · shield · money · chart · clock · hourglass · flag · star · target · pin · tag · computer · server · database · cloud · bulb · gem · anchor · pick · bank.
actorobject
Who the alert is about. Rendered as the author of the alert, and carried into the push notification and the email subject, so "New signup" arrives with a name on it. Send { name, email, id, plan, company, location, avatarUrl, url }, any subset of it, or just a string. Also accepted as user. From an email alone Zenhook fills in the display name, the initials, the company behind a work domain, and the Gravatar if the person has one.
fieldsarray
Key-value objects for structured data. Each object has label and value.name is accepted in place of label, and a value may be a number or a boolean.
linkUrlstring
URL for a clickable call-to-action link displayed below the title.
linkTextstring
Label for the link button. Defaults to "View" if linkUrl is set.
sourcestring
Author name shown in the alert header (e.g. "GitHub", "Stripe"). Defaults to "Webhook".
metadataobject
Arbitrary JSON object shown as an expandable "Raw Payload" section with copy-to-clipboard. Max 10 KB.
Errors

Errors are RFC 9457problem documents, served as application/problem+json. Branch on type, show title. The old error field is still there, so integrations written before the change keep working.

json
{
  "type": "https://zenhook.dev/problems/unknown-token",
  "title": "Invalid webhook token",
  "status": 404,
  "error": "Invalid webhook token"
}
Idempotency-Key

Send an Idempotency-Key header and a retry of the same request returns the first answer instead of creating a second alert, for 24 hours. The replay carries Idempotency-Replayed: true. A retry sent while the first request is still running gets a 409 rather than a duplicate. Worth setting on anything that retries on timeout, a deploy script above all.

Responses

201 Created

Alert received and queued for delivery. destinationnames the workspace and channel the token resolved to, so a misrouted integration shows up in the response instead of surfacing days later in the wrong channel.

json
{
  "success": true,
  "alertId": "clx1234567890abcdef",
  "destination": {
    "workspace": { "id": "ws_...", "name": "Acme Corp" },
    "channel": { "id": "ch_...", "name": "deploys", "slug": "deploys" }
  }
}

Use https://zenhook.dev/api/ for integrations. The www API alias also accepts requests without redirecting them. If a webhook or monitor heartbeat exceeds its request rate, the 429 response includes Retry-After: wait that many seconds before retrying. A monthly event allowance is separate; waiting a minute does not renew it.

Workflow notifications

A deploy or release is one process, not five alerts. Add aworkflow block and every POST that carries the samerunId updates a single alert in place: the card shows each step live, and the feed never fills with intermediate noise.

bash
# every POST of a run carries the same runId
curl -X POST https://zenhook.dev/api/webhook/YOUR_TOKEN \
  -H "Content-Type: application/json" \
  -d '{
    "title": "myapp deploy",
    "workflow": {
      "runId": "myapp-20260722-1015",
      "state": "running",
      "steps": [
        { "name": "test",   "state": "done",    "seconds": 46, "info": "716 tests" },
        { "name": "build",  "state": "running" },
        { "name": "deploy", "state": "pending" }
      ]
    }
  }'

States: the run is running, passed or failed; each step is pending, running, done, failed or skipped. Send the FULL steps array every time; the server replaces, it never merges.

Notification policy: the first POST notifies (started), running updates are silent, and the terminal state notifies once and resurfaces the alert as unread. The alert level is derived from the state, so a failed run can never claim success.

Metering: one process is one billable event, counted at the first POST. Updates are free.

On failure: pass workflow.error with the log tail; it renders on the card so the failure can be diagnosed from a phone.

Structured Data

Use the fields array to include rich metadata displayed in the dashboard and email alerts.

Example with fields
{
  "title": "Build Failed",
  "level": "error",
  "fields": [
    { "label": "Branch", "value": "main" },
    { "label": "Commit", "value": "abc123f" },
    { "label": "Author", "value": "John Smith" },
    { "label": "Error", "value": "npm install timed out after 300s" }
  ],
  "linkUrl": "https://github.com/org/repo/actions/runs/123",
  "linkText": "View Logs"
}

Widgets in the feed

There is nothing to configure. Zenhook reads each kind of event from its history and draws the widget that fits it. A kind of event is a stream: the same channel and the same title, numbers ignored, so Order 1284 paid andOrder 1285 paid are one stream. Keep the title stable and put what changes inmessage, fields oractor.

  • Counter, the default: any stream that repeats. Today's count, the change against yesterday at the same hour, bars per hour. Nothing to send.
  • Pipeline: a title with deploy, build, release, pipeline or CI, or a workflow block. The last runs and their pass rate. Send level: "error" when a run fails.
  • Error: level: "error". How many times today, since when, the message and your link.
  • Money: a field labeled Amount, Price, Total, Revenue or Paid, such as $49.00 or 49 EUR. The day's total.
  • Person: an actor (or user). Who, and how many today and this week.
  • Streak: an event that happens about once a day, such as a sync or a backup. Days in a row.
  • A one-off event stays a card with everything visible. Open a widget to read its events.
bash
curl -X POST https://zenhook.dev/api/webhook/YOUR_TOKEN \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Payment received",
    "level": "success",
    "actor": "[email protected]",
    "fields": [{ "label": "Amount", "value": "$49.00" }]
  }'
See every type, as it looks →

Reading alerts back out

The webhook endpoint above is public and write-only, anyone with the channel token can post into it. To list alerts, mark them read, or build an integration that consumes them, use the authenticated Read API with a Bearer token.