Skip to main content

Call lifecycle

Complete Calling setup first.
  1. Receive a calls change with event: "connect", direction: "USER_INITIATED", and an SDP offer.
  2. Create one media session for the call ID. Apply the offer and generate a real SDP answer.
  3. Start the audio pipeline without speaking and send pre_accept with that answer.
  4. Send accept when your media session is ready. Start the greeting only after accept succeeds and media is connected; pre_accept alone does not answer the call.
  5. Handle event: "terminate" by stopping the pipeline and closing the media connection.
The webhook may contain multiple entries, changes, calls, and statuses. Iterate over all of them and check 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.
This payload illustrates the structure; the SDP placeholder is not usable audio configuration. Preserve 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 in X-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.
Register this raw-body route before a global JSON parser. Return 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

Use X-API-Key for both actions. answer.sdp must come from the WebRTC session that applied this call’s offer.
For the SDK equivalents, see Calls. Your media server generates answer.sdp; see connecting a voice agent.

End or reject a call

When the caller hangs up, handle their 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.