Check relay configuration
GET /health includes TURN diagnostics when Calls configuration is loaded.
checks.turn.status is degraded when TURN URLs are set without a shared
secret; otherwise it is healthy. checks["turn.rotation"].status is
in_progress while a retiring shared secret remains configured and none
after the overlap ends.
These fields inspect API configuration. They do not connect to the TURN
listener or test DNS, allocation continuity, or ICE recovery. An HTTP 200
means the required readiness checks passed even when an optional diagnostic
reports degraded or in_progress.
Choose how a session answers
Set the answer mode once per session withPOST /messaging/voip/mode:
Receive a call
Every inbound call raises acall.received webhook carrying the callId. In
sdk mode the call keeps ringing until you decide:
POST /messaging/voip/calls/{id}/acceptanswers it. Send{"video": true}to take the caller’s video too; an audio-only call stays audio.POST /messaging/voip/calls/{id}/rejectdeclines it.
202 and 409 when the call is no longer ringing — it was already
answered, ended, or the session auto-answers. A call nobody decides on within
the ring window ends with a call.missed webhook.
Place a call
201 response carries the callId while the callee’s device rings.
call.accepted follows when they pick up and call.ended when either side
hangs up; a call that rings out ends with reason ring_timeout. to is an
E.164 phone number or a user ID. Hang up with
DELETE /messaging/voip/calls/{id}.
Outbound starts share a per-minute limit for each source session in your
organization. Changing the destination or using another server key or client
token does not reset that limit. A placement above the limit returns 429
without ringing the destination. Wait for the next minute before retrying.
Client-token setup and concurrency rules also apply. Answering an inbound call,
attaching media, and hanging up do not consume this outbound-start limit.
Invite more people
POST /messaging/voip/calls/{id}/participants with {"to": "+15550199"} rings
another person into a live call, turning a one-to-one call into a group call.
The response describes the invitee as invited, or its newer roster state if it
changed before the invitation completed. A left invitee is absent from the active
roster. Later roster changes
reach your media socket as participant_joined, participant_state, and
participant_left frames. Browser clients receive the same changes as
call.participant_joined, call.participant_state, and
call.participant_left lifecycle events. The browser feed is ephemeral: it
does not replay roster changes missed during a disconnect. The audioMuted
and video fields are reserved and remain false because WhatsApp does not
expose either state per participant.
Attach media programmatically
To hear and speak on a call from your own code:- Mint a per-call ticket with
POST /messaging/voip/calls/{id}/agent-token(team and project keys only). Project-key tickets bind to the call’s current session and generation. They are rejected if either changes, including when another call replaces the same call ID. Team-key tickets allow team-wide access and validate the call, team, and expiry. Tickets expire afterttlSeconds(default 300). - Open a WebSocket to the
urlreturned with the ticket or, when the response has none, to/voip/sdk?callId={id}on the API host. Request the subprotocols in this order:pmfa.calls.v1, thenpmfa.ticket.<token>. The response selects onlypmfa.calls.v1; it never echoes the ticket. - Wait for the first text frame,
{"type":"ready","sampleRate":16000,"video":false}, before sending media.
401 before the connection opens. Expiry does not close an
already connected call; obtain a fresh ticket before reconnecting after expiry. Unavailable call media returns
409. Keep each frame at or below 2 MiB and send no more than 500 frames per
second. Close the socket when your integration stops using it.
Frames on the socket:
Browsers use WebRTC instead: submit an SDP offer with
POST /messaging/voip/calls/{id}/offer and trickle candidates with /candidate. See
the API methods for the browser
flow.
The calls signaling socket binds each call ID to its current generation when
it first receives a candidate or teardown command. Later commands on that
socket keep the same binding. Reconnect before controlling a replacement that
reuses the call ID. After 256 distinct call bindings, the socket closes with
code 1013 and asks the client to reconnect; it does not discard older bindings.
Webhooks
Call history
Call history can lag behind live events during a temporary recording failure. Failed writes are retried for up to 24 hours. A delayed offer does not move an accepted or finished call back to ringing. The finalcall.ended event supplies
the call direction and duration.
Client tokens
Every call-ID operation stays within the credential’s resource boundary. Organization keys can control calls across their organization. Project keys can control calls on any session owned by that project. Client tokens can control calls only on their bound session, and that session must still belong to the token’s project. A call outside the boundary returns409 before the
operation changes or drains call state.
A client token needs the matching action: voip_place to place calls, invite
participants, or submit an offer; voip_answer to accept or reject; and
voip_signal for ICE candidates and hang-up. Either voip_place or
voip_answer may set the answer mode. Client rules apply to placements and
invites: max_setups_per_minute (default 10), allowed_number (checked against
to), and max_concurrency (a placed call holds a slot until it ends). Exceeding
a rule returns 429.
Call completion releases its concurrency slot even if your client disconnects
without sending a hang-up request. Other active calls keep their slots. A call
that ends before its placement response arrives does not keep a slot reserved.
A lost media host also ends the known call and releases its slot. A slot expires
after four hours only when Polymorfa cannot associate the reservation with a
known call.
A failed reject or hang-up request does not free capacity. A confirmed setup
refusal releases its new reservation while preserving any existing call.
Errors
If placement or call setup fails after dispatch without a confirmed outcome,
its client-token concurrency slot remains reserved for up to four hours. The call
can still be active when the response was lost. A known call releases its slot
when it ends; an unknown placement remains reserved until expiry.