message.received event to your webhook subscriptions. Events are durable, signed, and replayable.
| Event type | When it fires | Latency | Use when |
|---|---|---|---|
message.received | Real-time on every inbound iMessage | Seconds | Building a live agent or chat UI. Recommended default. |
events: ["message.received"] on POST /api/v1/webhook-subscriptions.
Configuring a webhook
Two options:- Console — your Chert administrator configures a webhook endpoint for your tenant.
- API — create a subscription programmatically with
POST /api/v1/webhook-subscriptions. See Webhook subscriptions.
signing_secret — rotate either without touching the other.
If you cannot expose a public URL during development, use the SSE stream or dev inbox instead.
message.received envelope
Real-time, fires immediately when an inbound iMessage lands.
message.received
Field map
| You want | Path |
|---|---|
| Event type | event (also in the X-Webhook-Event header) |
| Dedupe key | event_id (also in X-Webhook-Event-Id) |
Chat id (use with /chats/{id}/... endpoints) | data.chat.id |
| Tenant slug | partner_id |
| Sender phone | data.message.sender_handle.handle |
| Your line phone | data.chat.owner_handle.handle |
| Chat handles / participants | data.chat.handles |
| Inbound text | data.message.parts.find(p => p.type === "text").value |
| Inbound attachment metadata | data.message.parts.filter(p => p.type === "media") |
| Inbound attachment bytes | GET /api/v1/attachments/{attachment_id}/content |
Inbound message id — valid input to /messages/{id}/react | data.message.id |
| Sender line | data.chat.owner_handle.handle |
| Group flag | data.chat.is_group |
Compat note. Some legacy deliveries also include top-levelfrom,to,text,message_guid, andline_idaliases. New integrations should prefer thedata.*paths because they are richer and consistent across Chert-managed lines.
Receiving images and attachments
Inbound media is handled by the Chert API layer automatically. When a lead sends an image or file, themessage.received webhook carries a media part in data.message.parts[] with an attachment_id, filename, MIME type, and byte size. Fetch the bytes with the same tenant auth you use for other API calls:
data.message.parts[] when reconstructing the message.
Replying
Two working paths. Pick by what you need:$CHAT_ID is data.chat.id from the webhook. $MESSAGE_GUID is message_guid (or data.message.id) and can be used for tapbacks with /messages/{id}/react. Use a normal chat follow-up for replies.
Headers
These headers are sent by Chert to your webhook endpoint during event delivery. Do not sendx-chert-tenant in your webhook receiver — that header is only for requests your application makes to Chert APIs.
Recommended webhook delivery headers
| Header | Format | Description |
|---|---|---|
X-Webhook-Event | string | Event type, e.g. message.received. |
X-Webhook-Event-Id | string | Stable identifier. Use as a dedup key. |
X-Webhook-Timestamp | unix seconds | Timestamp used in the signature payload. |
X-Webhook-Subscription-Id | string | Id of the subscription that triggered this delivery. |
X-Webhook-Signature | t=<ts>,v1=<hex> | HMAC signature computed with the subscription secret. |
content-type | application/json | Always application/json. |
Legacy compatibility headers
Chert also emits these older lowercase headers for existing integrations. New integrations should read the recommendedX-Webhook-* headers above.
| Header | Format | Description |
|---|---|---|
x-chert-event | string | Same event type as X-Webhook-Event. |
x-chert-event-id | string | Same dedup key as X-Webhook-Event-Id. |
x-chert-timestamp | unix seconds | Same timestamp as X-Webhook-Timestamp. |
x-chert-signature | v1,<ts>,<hex> | Legacy HMAC signature format. |
HMAC-SHA256(subscription_secret, "<ts>.<raw_body>").
Verifying the signature
Compute the HMAC over the raw request body (exact bytes as received) and compare with constant-time equality. Do not parse and re-serialize the JSON before verifying.401.
Retry behavior
| Response from your endpoint | Chert action |
|---|---|
2xx | Marked delivered. |
5xx or network error | Up to 3 total delivery attempts with short exponential backoff. |
4xx | Not retried during that delivery. Marked failed if no matching destination succeeds. |
GET /api/v1/events and replay manually after fixing your endpoint.
At-least-once semantics
You may receive the same event more than once during retries or replays. Always dedup onevent_id (also exposed in the x-chert-event-id header).
Replaying missed events
If your endpoint was down, list events withdelivery.status = failed and replay them:
?subscription_id=<subscription_id>.
See Events for the full debugging surface.

