Read API

The webhook endpoint is public and write-only — it's how your apps push alerts in. To read alerts back out (build a CLI, sync to another tool, integrate with a dashboard), use the authenticated API with a Bearer token.

Authentication

All read endpoints require a Bearer token. Tokens are scoped to a single (user, workspace) pair — the user's channel access (including private channels they belong to) is inherited automatically.

Generate a token

From the dashboard: Settings → API Tokens → New Token. The full token is shown once at creation — copy it immediately. Zenhook stores only a SHA-256 hash, so a lost token can't be recovered (revoke and create a new one).

Token format
zhk_<48 hex chars>
# example: zhk_a1b2c3d4e5f6...

Sending the token

bash
curl https://zenhook.dev/api/channels \
  -H "Authorization: Bearer zhk_..."

List channels

Returns the channels the token's user can access (every public channel plus any private channel they're an explicit member of). Each channel includes an _count.alerts field containing the user's personal unread count.

GEThttps://zenhook.dev/api/channels

Responses

200 OK
json
[
  {
    "id": "clx_chan_1",
    "name": "production-errors",
    "slug": "production-errors",
    "icon": "🔴",
    "isPrivate": false,
    "webhookToken": "abc123...",
    "_count": { "alerts": 3 }
  }
]

List alerts in a channel

Paginated, newest first. Each alert includes an isRead boolean computed for the calling user — marking an alert read affects only your own state.

GEThttps://zenhook.dev/api/channels/{id}/alerts

Query parameters

cursorstring
Alert ID returned as nextCursor in the previous page. Omit for the first page.
limitnumber
1–100. Defaults to 50.
levelenum
Filter by severity: info, success, warning, error.
afterstring
Alert ID. Returns alerts strictly newer — used for polling.

Responses

200 OK
json
{
  "alerts": [
    {
      "id": "clx_alert_1",
      "channelId": "clx_chan_1",
      "title": "Build Failed",
      "level": "ERROR",
      "emoji": "🔴",
      "message": "npm install timed out after 300s",
      "fields": [{ "label": "Branch", "value": "main" }],
      "linkUrl": "https://github.com/org/repo/actions/runs/123",
      "isRead": false,
      "createdAt": "2026-04-27T14:32:11.000Z"
    }
  ],
  "nextCursor": "clx_alert_42",
  "hasMore": true
}

Mark as read

Read state is per-user. Marking an alert read inserts a row scoped to the token's user — other workspace members' unread counts are unaffected.

PATCHhttps://zenhook.dev/api/alerts/{id}
bash
curl -X PATCH https://zenhook.dev/api/alerts/clx_alert_1 \
  -H "Authorization: Bearer zhk_..." \
  -H "Content-Type: application/json" \
  -d '{ "isRead": true }'

Mark every alert read

POSThttps://zenhook.dev/api/alerts/mark-all-read

Body is optional. Pass { "channelId": "..." } to scope to one channel; omit it to mark every accessible channel as read. Idempotent — re-running is a no-op.

Get a single alert

GEThttps://zenhook.dev/api/alerts/{id}

Returns the same alert shape as the list endpoint plus the channel's name / slug.

Errors

401 Unauthorized — missing or invalid Bearer token, or the token's user is no longer a workspace member.

404 Not Found — the resource doesn't exist or the token's user can't access it (e.g. private channel they're not on). The two cases are conflated on purpose so token holders can't enumerate private channels.

429 Too Many Requests — rate limit hit. Back off and retry.