Call lifecycle
Complete Calling setup first.- Receive a
callschange withevent: "connect",direction: "USER_INITIATED", and an SDP offer. - Create one media session for the call ID. Apply the offer and generate a real SDP answer.
- Start the audio pipeline without speaking and send
pre_acceptwith that answer. - Send
acceptwhen your media session is ready. Start the greeting only afteracceptsucceeds and media is connected;pre_acceptalone does not answer the call. - Handle
event: "terminate"by stopping the pipeline and closing the media connection.
value.metadata.phone_number_id before routing. Call statuses such as RINGING, ACCEPTED, and REJECTED arrive in value.statuses[] on the calls field. Their id identifies the call; outbound calls depend on these updates.
from_user_id when present. Do not require from to contain a phone number; see business-scoped user IDs.
Verify the webhook
Kapso signs forwarded Meta webhooks with a hex HMAC-SHA256 inX-Webhook-Signature, using your webhook’s secret_key. Verify the exact request bytes before decoding JSON. This is not Meta’s X-Hub-Signature-256 scheme and does not use your Meta app secret.
This Express handler verifies and extracts events. Implement enqueueCallEvent and enqueueOtherMetaChange using your application’s queue or router. They must durably accept events before you return 200; do not start a conversation inside the receiver.
200 within the 10-second webhook timeout; session startup and the conversation run outside that HTTP request. Preserve existing handling for other Meta fields if you share the receiver with messaging. If normal messages are already processed from a Kapso-format webhook, avoid processing them again from the Meta receiver; still handle interactive.call_permission_reply here.
Pre-accept and accept
UseX-API-Key for both actions. answer.sdp must come from the WebRTC session that applied this call’s offer.
answer.sdp; see connecting a voice agent.
End or reject a call
terminate event without starting another session or attempting to accept the call. When your agent ends a call, finish its farewell audio, send terminate, and close the media connection. Ending an AI pipeline alone does not end the WhatsApp call.
Deduplicate each queued event using its delivery key plus payload position, as in the receiver above. Separately, guard session creation using the Meta call ID (call.id or status.id). Keep a terminal call state so a late or repeated connect cannot restart a terminated call. With multiple workers, use shared state and route subsequent events to the worker that owns the live media session.
If the delivery header is missing, skip delivery-key deduplication rather than creating keys that collide across requests. The per-call session and terminal-state guards still apply.
The delivery key belongs to the whole webhook body. If you enqueue events separately, combine it with each event’s position in the payload; using the header alone as a unique queue key would discard other calls in the same delivery.
