Chat API & Webhooks
Two ways to wire conversations into your own software: talk to the assistant directly over HTTP (drive it from your own UI, test prompts from CI, let another system converse with it), and be told when conversations finish via a webhook (post transcripts to your CRM, trigger follow-ups, feed analytics).
Available to any signed-in member of the account that owns the app,
and to scoped API keys with the chat capability. Runs a
synchronous chat exchange against the app's prompt.
Response (200):
Pass "chatId" on subsequent calls to maintain
conversation context.
curl -X POST https://api.everycallhandled.com/api/v1/apps/$APP_ID/chat \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{ "message": "Hello, can I book an appointment?" }'{
"reply": "Of course! What date and time works for you?",
"chatId": "..."
}Apps can configure a postChatCallback attribute that
fires after each completed conversation. Useful for:
- Posting transcripts to your CRM
- Triggering follow-up automation
- Custom analytics
Set the callback by PATCHing the
app:
Payload (POSTed to your URL after each call):
{
"attributes": {
"postChatCallback": {
"uri": "https://your-server.com/webhook",
"method": "POST",
"headers": { "Authorization": "Bearer YOUR_INTERNAL_TOKEN" }
}
}
}{
"appId": "...",
"chatId": "...",
"callerIdPhone": "+44...",
"callbackPhone": "+44...",
"callerName": "...",
"callerEmail": "...",
"summary": "Caller booked an appointment for...",
"urgency": "normal",
"sentiment": 8,
"callDuration": 145,
"transcript": [ { "speaker": "...", "text": "...", "timestamp": "..." } ],
"patientContext": { /* populated when the Semble integration is connected */ },
"bookingsMade": [ /* if a booking integration is connected */ ]
}Timeout: each delivery attempt times out after
5 seconds. Your endpoint should acknowledge with a 2xx
response inside that window — even a bare 200 OK is fine;
we don't need a body. Slow processing (writing to your CRM, triggering
downstream automations) should happen asynchronously after you've
already responded; treating webhook delivery as an inbox-write pattern
is the safest shape.
Retry policy: failed deliveries (any non-2xx response, connection refused, or timeout) are retried with exponential backoff:
| Attempt | Delay after previous |
|---|---|
| 1 | immediate (first try) |
| 2 | ~30 seconds |
| 3 | ~2 minutes |
| 4 | ~10 minutes |
| 5 | ~1 hour |
| 6 (final) | ~6 hours |
After 6 failed attempts, the platform stops trying for that conversation and records the failure internally. We do not notify your account by default; if you need failure notifications, talk to support.
Idempotency: every delivery carries the same
(appId, chatId) tuple — if you receive the same payload
twice (e.g. a retry fired even though your endpoint succeeded but the
connection dropped before we read the response), de-duplicate on
chatId. Payload content never changes between retries, so a
second delivery for a known chatId is safe to drop.
Signature verification: the platform signs each
request with HMAC-SHA256 over the JSON body, included as an
X-ILQ-Signature header. The signing key is generated when
you configure the callback and returned as the
webhookSigningSecret field in the
PATCH /apps/{id} response that sets
postChatCallback (it starts with whsec_);
store it server-side. If you set the callback up before signing was
available, re-save it once (any PATCH that sets
postChatCallback) to generate the key — deliveries stay
unsigned, exactly as before, until you do. Verify on receipt:
Without verification you can't tell a real platform delivery from a third party who's discovered your callback URL. We strongly recommend verifying every payload.
Failure-mode summary:
| Scenario | What happens |
|---|---|
| Your endpoint returns 2xx | Delivery succeeded; no retry. |
| Your endpoint returns 4xx | Treated as permanent failure; retries cease after attempt 1. (Reason: 4xx usually means malformed payload from our side; retrying won't help.) |
| Your endpoint returns 5xx | Retried per the backoff table above. |
| Connection refused / DNS failure | Retried per the backoff table above. |
| Timeout (>5s, no response) | Retried per the backoff table above. |
| All 6 retries exhausted | Recorded on our side; not delivered to your account. |
The call itself succeeds regardless of webhook
outcome. Webhook delivery is asynchronous to the call; a failed
webhook never affects the caller's experience or your billing. The
transcript + summary + analytics are still stored in your account and
queryable via the /conversations endpoints — the webhook is
a convenience, not the source of truth.
Other events: the post-call webhook is the only
customer-subscribable push notification today. For billing and usage
events, poll GET /api/v1/billing/merchant or the ledger
endpoints on your own schedule. A general-purpose subscribable event
stream is on the roadmap but not shipped.
import hmac, hashlib
expected = hmac.new(signing_key.encode(), request.body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get('X-ILQ-Signature', '')):
return 401