Skip to documentation
Notify Docs

Webhook reference

Exact notification routes, payload fields, HTTP outcomes, and receiver formats. Use the task guides for a first send or question; use this chapter when wiring a sender or callback receiver.

Notification routes

Notification API routes
RoutePurpose
POST /hooks/:tokenSend a notification.
GET /hooks/:token/events/:eventIdRead the state of an interactive response.
POST /hooks/:token/events/:eventId/cancelWithdraw a pending interactive response.
POST /hooks/:token/events/:eventId/withdrawRequest removal of a delivered notification.

All webhook writes use JSON with Content-Type: application/json. The token identifies the owning service. Invalid JSON fields return 400 and an issues array; a token that does not resolve returns 404. Keep event IDs from successful sends for the event-specific routes.

Notification fields

Notification request fields
FieldTypeDescription
bodystring, requiredTrimmed nonempty message: at most 8,000 UTF-16 code units and 16,384 UTF-8 bytes. Including response reduces the code-unit limit to 2,000.
titlestringOptional sender title for this message; maximum 80 characters. Otherwise use the service title.
imageUrlstringOptional avatar, maximum 2,048 characters. Requires public HTTPS; local names and loopback, private, or link-local addresses are invalid.
urlstringTap destination, maximum 2,048 characters. Accepts web links, universal links, documented app schemes, and iOS Shortcuts URLs.
deviceIdsstring[]Select 1–50 device IDs instead of the default active-device fanout. Exclusive with group and oncall.
groupstringWorkplace device-group ID (dgrp_…). Requires team membership and group targeting permission. Exclusive with deviceIds and oncall.
oncallstringWorkplace rotation ID (ocg_…). Creates a page with a body up to 2,000 characters; exclusive with deviceIds, group, and response. Families cannot page.
responseobjectQuestion configuration: approval, yes/no, or typed answer. Exclusive with oncall and appId.
projectstringInbox project name, maximum 80 characters. The first matching send creates the project if capacity permits.
summarystringBanner/list digest, maximum 500 characters. The full message remains in the inbox.
bodyFormatenumStored format marker: text (default) or markdown. V1 clients still display plain text.
appIdstringRegistered hosted-app ID (app_…) available to the sender. Tap opens that app; an optional url must match its origin. Exclusive with response.

HTTP results

json
{
  "ok": true,
  "eventId": "evt_example_pickup",
  "delivered": 1
}
Notification status codes
FieldTypeDescription
200okAccepted, or an idempotent replay.
202okAn identical request is still processing.
400errorInvalid payload, key, or device selection.
404errorUnknown webhook token.
409errorIdempotency key reused with a new payload.
429errorRate limit exceeded.
502errorAll targeted push requests failed with the configured providers.

Provider diagnostics belong in the dashboard activity log because raw errors may contain device push credentials. Inspect that log when a provider rejects a target; the caller receives the public status shape instead of sensitive provider output.

Per-question callbacks

A per-question callback receives answered outcomes as JSON with user agent Notify-Callbacks/1. A classic { url, token } callback uses a public HTTPS destination and Authorization: Bearer <callback.token>. A connector { target: { connectorId, route, path } } resolves the active clawchest.com address when delivery runs. Both carry Notify-Timestamp and Notify-Signature: v1=<hex> over {timestamp}.{rawBody}. Redirects are refused and each request times out after 10 seconds.

json
{
  "type": "notification.response",
  "interactionId": "int_example_pickup",
  "actionDigest": "<the stored interaction actionDigest>",
  "eventId": "evt_example_pickup",
  "correlationId": "pickup-42",
  "kind": "approval",
  "status": "approved",
  "action": "approve",
  "text": null,
  "respondedAt": "2026-10-11T18:06:52.000Z"
}

The callback’s kind maps a text request to reply; approval and yes_no keep their names.

Correlate and deduplicate answers using interactionId. The existing eventId and correlationId remain available for service notifications, but can be null for agent-created asks. actionDigest identifies the exact stored Notify prompt; it is not proof that an external action executed. Accept additional JSON fields when reading callbacks. The callback tries immediately and, on non-2xx failure, again after 30 seconds, 2 minutes, 10 minutes, and 1 hour. Five failed attempts exhaust delivery. Store an outcome durably before returning 2xx.

This callback only reports a person’s answer. It does not report expired or canceled questions. Poll the event route for those states, or use an account reply webhook for interaction lifecycle events.

Account reply webhooks

Use an account reply webhook when several tools should report interaction outcomes to one receiver. It subscribes to interaction.answered, interaction.expired, and/or interaction.canceled. Create a named HTTPS endpoint in the dashboard and enable it. Use for account replies makes it the single account default, including service-free MCP/API prompts. API and MCP updates use isDefault: true; false clears that endpoint’s default status. Selecting a new default replaces the old one.

For each outcome, Notify considers the enabled account default, the interaction’s explicit webhookId, and the service’s current defaultReplyWebhookId. Each distinct subscribed endpoint receives one durable delivery record; repeated selection of the same endpoint does not duplicate that record. Network retries may still reach a receiver more than once. A service’s selected webhook must have the same owning account.

Select Generic HTTPS, grok, or Slack. New dashboard endpoints initially select answered, expired, and canceled outcomes; editing an existing endpoint preserves its selected outcomes. A grok routine supplies its webhook URL and Authorization header. Slack incoming webhooks receive a JSON text field along with delivery/signature headers.
For service-free MCP or API questions, enable Use for account replies on the endpoint and subscribe to the desired outcomes. This avoids attaching webhookId to each question.
Add receiver-specific custom headers if needed. Reserved security/transport headers cannot be replaced. Header values are encrypted and read back as ••••••••; either preserve saved headers or replace the entire list with actual values, never the mask.
Store the one-time signing secret at the receiver. Rotation supplies a replacement and makes the old secret unusable; update the receiver accordingly.
Use Test & logs to queue an interaction.answered sample with test: true (202 means queued, not delivered). Inspect up to 50 recent deliveries, including latency, response code, and saved attempt details. Older deliveries may lack per-attempt history.

Manage these endpoints with MCP webhooks_create, webhooks_list, webhooks_get, webhooks_update, webhooks_delete, webhooks_test, and webhooks_rotate_secret, or the dashboard /api/reply-webhooks routes. Create and rotate return the signing secret once. Get includes the last 50 deliveries. A delivery’s interactionId links it to the originating ask; test deliveries use null. Older servers may omit that field. An account can have up to 25 endpoints. Agent endpoint reads require services:read; writes require services:write.

Before parsing a request, verify X-Notify-Signature: t=<unix>,v1=<hex>. Sign the timestamp, a dot, and the unchanged body bytes with HMAC-SHA256 and the endpoint’s signing secret. Reject future timestamps and timestamps older than five minutes, then compare digests in constant time. Parsing and reserializing JSON cannot preserve this signature input.

Node receiver helper; supply the original body bytes as a Buffer:

javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyNotify(rawBody, signature, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof signature !== "string") return false;
  const match = /^t=([0-9]+),v1=([a-f0-9]{64})$/.exec(signature);
  if (!match) return false;
  const [, timestamp, hex] = match;
  const now = Math.floor(Date.now() / 1000);
  const age = now - Number(timestamp);
  if (!Number.isSafeInteger(Number(timestamp)) || age < 0 || age > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(timestamp + ".")
    .update(rawBody)
    .digest();
  return timingSafeEqual(expected, Buffer.from(hex, "hex"));
}

// Use a raw-body middleware before any JSON parser:
// verifyNotify(req.body, req.get("X-Notify-Signature"), process.env.NOTIFY_WEBHOOK_SECRET)
// Only after verification: JSON.parse(req.body.toString("utf8"))

Python receiver helper; supply the original body bytes as bytes:

python
import hashlib
import hmac
import re
import time


def verify_notify(raw_body: bytes, signature: str, secret: str) -> bool:
    if not isinstance(raw_body, bytes) or not isinstance(signature, str):
        return False
    match = re.fullmatch(r"t=([0-9]+),v1=([a-f0-9]{64})", signature)
    if not match:
        return False
    timestamp, received = match.groups()
    age = int(time.time()) - int(timestamp)
    if age < 0 or age > 300:
        return False
    signed = timestamp.encode("ascii") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)

# Read the raw body before calling the framework's JSON parser.
# After verification, decode with json.loads(raw_body).
Use durable deduplication after signature verification. X-Notify-Delivery-Id identifies a delivery across retries; commit its record together with the side effect. Already processed IDs should return 2xx. Retries preserve the ID and body but refresh the signature timestamp. Tests and separate lifecycle outcomes receive separate IDs.

Reply webhooks try immediately and then after 1, 5, 15, and 40 minutes. Timeouts and 5xx responses retry; 4xx responses and redirects are final failures. Any 2xx acknowledges delivery. Disabling stops new deliveries. These endpoints can coexist with classic callbacks, but their payloads, signature headers, and retry schedules differ.