message.received event to your webhook subscriptions. Events are durable,
signed, and replayable.
Subscribe by passing
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
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, recording, or file,data.message.parts[] carries an
attachment part. A fetchable part includes attachment_id; filename,
mime_type, and size_bytes are metadata hints and may be absent or generic.
Fetch the bytes with the same tenant auth you use for other API calls:
data.message.parts[] when reconstructing the message.
Receiving inbound voice memos
There is no separate inbound voice-note endpoint. An Apple voice memo arrives through the normalmessage.received workflow and is retrieved through the
same authenticated attachment-content route as other inbound media.
The following representative event reflects a verified case where the event
contained a generic MIME type and no filename:
message.received voice memo
The event may describe an inbound recording as
media or another
attachment-like part. Chert does not currently expose a universal provenance
flag that proves an audio attachment originated from Apple’s native voice
memo UI. Use attachment_id to retrieve it, then inspect the response and
bytes.Download with cURL
Bearer or HMAC authentication is required. For multi-tenant accounts, includex-chert-tenant; default single-tenant bearer requests may omit it.
--remote-header-name tells cURL to use the filename from
Content-Disposition. In the verified case, the response included headers
equivalent to:
Content-Type did not make the audio invalid. The downloaded
bytes were a playable CAF container with Opus audio.
Download with TypeScript
Download with Python
Inspect and convert the audio
Inspect the file signature and stream metadata instead of trusting only the webhook MIME type:caff) containing mono
Opus audio, approximately four seconds long at about 32 kbps. Apple inspection
reported 24 kHz Opus while FFmpeg exposed the decoded stream at 48 kHz. Do not
assume every Apple device or OS version produces this exact combination.
Convert CAF/Opus to a mono 16 kHz WAV for tools that do not accept the original
container or codec:
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
Legacy compatibility headers
Chert also emits these older lowercase headers for existing integrations. New integrations should read the recommendedX-Webhook-* headers above.
Both signature formats compute the same HMAC:
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
Failed events may be retried later by Chert. Pull events through
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, replays, or attachment-metadata repairs. Useevent_id (also exposed in the
X-Webhook-Event-Id header) to make side effects idempotent, but merge a
redelivery when it contains richer attachment or group metadata.
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.

