Skip to main content
When a lead replies from either a phone number or Apple ID email handle, Chert creates or matches the tenant-scoped chat and delivers a real-time 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. Event lifecycle

Configuring a webhook

Two options:
  1. Console — your Chert administrator configures a webhook endpoint for your tenant.
  2. API — create a subscription programmatically with POST /api/v1/webhook-subscriptions. See Webhook subscriptions.
The webhook subscription secret is independent from your API 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-level from, to, text, message_guid, and line_id aliases. New integrations should prefer the data.* 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:
The attachment id is tenant-scoped. If an inbound contains both text and media, keep the array order from 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 normal message.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, include x-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:
The generic 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:
One verified sample was Apple CAF/Core Audio Format (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:
Chert preserves the downloaded recording; it does not automatically convert inbound audio to MP3 or WAV. No transcript field is currently returned, and Chert does not perform automatic speech-to-text or semantic interpretation. Applications that need a transcript must send the downloaded audio to their chosen speech-to-text system.

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 send x-chert-tenant in your webhook receiver — that header is only for requests your application makes to Chert APIs.

Legacy compatibility headers

Chert also emits these older lowercase headers for existing integrations. New integrations should read the recommended X-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.
Reject any request that fails verification with 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. Use event_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 with delivery.status = failed and replay them:
To replay to one subscription instead of every matching active subscription, add ?subscription_id=<subscription_id>. See Events for the full debugging surface.

See also