People Call API Overview
One API for a team member's calls with your customers: calls out and calls in, on WhatsApp or on a phone line (PSTN).
One API for a team member's calls with your customers: calls out and calls in, on WhatsApp or on a phone line (PSTN). You say where the customer is (channel) and how the team member talks (mode).
Call out (WhatsApp, VOIP) → POST /v1/call/person → credentials → your app joins → the customer's WhatsApp rings
Call out (phone, DIRECT) → POST /v1/call/person → the team member's phone rings → then the customer's → connected
Call in (WhatsApp, VOIP) → poll /updates every 2 s → a call rings → POST /:callId/accept → credentials → your app joins
Hang up → POST /:callId/end → the call ends for everyone
What Is Supported
| channel | mode | What happens | Status |
|---|---|---|---|
WHATSAPP | VOIP | The team member talks from your app (browser, server or mobile), using the credentials the API returns. | Available |
PSTN | DIRECT | The team member's own phone rings first; when they pick up, the customer is dialled and the two are connected. | Available |
PSTN | VOIP | — | Not yet: 400 MODE_NOT_SUPPORTED |
WHATSAPP | DIRECT | — | Not yet: 400 MODE_NOT_SUPPORTED |
Incoming WhatsApp calls are answered through the API (VOIP). Incoming phone (PSTN) calls are answered on the team member's phone.
The Basics
- Base URL:
https://public.doubletick.io - Authentication: your DoubleTick API key in the
Authorizationheader, as it is, with noBearerin front. A missing or wrong key gets401. - Content type:
application/json. - The team member is named by
agentNumber: the phone number on their DoubleTick profile, with country code. It must match exactly one active team member of your organization. Every permission check is done for that team member, not for the API key's owner. - Phone numbers take the country code, with or without a leading
+(7–15 digits). - One call at a time: a team member can be on one call at once.
- Unknown fields are refused (
"<field>" is not allowed).
NOTE
If you do not have your DoubleTick API Key, you can obtain it by following this guide link.
Reply Shape
{ "success": true, "message": "...", "data": { ...the call... }, "credentials": { ... } }
{ "status": 400, "reason": "MODE_NOT_SUPPORTED", "message": "..." }
datais always the call, read fresh (see The call object). A call is the same object on every route.credentialscome only on a VOIP call out, accept and reconnect (see Joining a VOIP call).
Endpoints
| Endpoint | Body | What it does |
|---|---|---|
POST /v1/call/person | see Call a Customer | Call a customer. |
POST /v1/call/person/updates | agentNumber | Keeps the team member reachable for incoming WhatsApp calls, and lists the calls ringing for them and the calls they are on. |
POST /v1/call/person/{callId}/accept | agentNumber, mode (optional, VOIP) | Answer a WhatsApp call ringing for the team member. |
POST /v1/call/person/{callId}/decline | agentNumber | Refuse a call ringing for the team member. It ends for everyone it rang. |
POST /v1/call/person/{callId}/reconnect | agentNumber | New credentials after the team member's connection dropped. Within 60 seconds of the drop. |
POST /v1/call/person/{callId}/end | agentNumber | Hang up a call the team member is on, is placing, or has accepted. |
GET /v1/call/person/{callId} | — | Read the call. |
{callId} is the id from a reply, from /updates, or from your call webhooks. A call of another organization, or a call no team member is on (for example an AI agent's call), gets 404 Call not found.
Typical Flows
A team member calls a customer on WhatsApp
POST /v1/call/person {channel: WHATSAPP, mode: VOIP, from, to, agentNumber}- join with the credentials (see Joining a VOIP call) → the customer's WhatsApp rings
- the customer answers → talking
POST /v1/call/person/{callId}/end→ close the audio connection
A customer calls on WhatsApp and a team member answers
POST /v1/call/person/updates {agentNumber}, every 2 s → a call appears,callStatusINITIATED,callDirectionINCOMINGPOST /v1/call/person/{callId}/accept {agentNumber}→ credentials- join with the credentials → talking (or
POST …/declineto refuse it) POST /v1/call/person/{callId}/end→ close the audio connection
A team member calls a customer on a phone line
POST /v1/call/person {channel: PSTN, mode: DIRECT, from, to, agentNumber}- the team member's phone rings → they pick up → the customer is dialled → connected
- either side hangs up, or
POST /v1/call/person/{callId}/end
The Call Object (data)
Field names match the call webhooks you already receive; the callId is the same.
| Field | What it holds |
|---|---|
callId | The call's id. |
channel, mode | WHATSAPP · VOIP, or PSTN · DIRECT. |
callDirection | INCOMING or OUTGOING. |
callStatus | INITIATED, IN_PROGRESS, END, MISSED, REJECT, FAILED. |
from, to | Set by direction, as in the webhooks. |
agentNumber | The team member who took, is on, or declined the call; null before anyone has. |
customerName, customerNumber, customerUserId | The customer. customerUserId is the WhatsApp user id when WhatsApp does not share the caller's number. |
whatsappNumber, chatId | The chat the call is logged in. whatsappNumber is set on PSTN calls only. |
ringExpiresAt | For an incoming WhatsApp call that can still be accepted: until when. Otherwise null. |
callStartedAt, callEndedAt, durationInSeconds, recordingUrl, failedReason | Filled a moment after the call ends. |
Joining a VOIP Call with the Credentials
A VOIP call out, accept and reconnect return credentials:
{
"transport": "SIP",
"wssUrl": "wss://...",
"sipUri": "sip:...",
"username": "...",
"password": "...",
"headers": { "...": "..." },
"iceServers": [],
"codecs": ["opus", "G722", "PCMU", "PCMA"],
"expiresAt": "2026-10-26T10:00:30.000Z"
}
They open one voice session over a secure WebSocket, using the standard SIP-over-WebSocket protocol with WebRTC audio. Libraries such as JsSIP or SIP.js (browser and Node.js) and PJSIP (mobile and server) do this.
- Open a WebSocket to
wssUrl. Do not register. - Send one call (INVITE) to
sipUri. Add every header inheaders, and answer the login challenge withusernameandpassword. - Audio only. Offer the
codecsin the order given, Opus first. - Use ICE with DTLS-SRTP and rtcp-mux, and the
iceServersexactly as given (the list may be empty). - Send a keep-alive every 20–30 seconds while the call is up.
- Turn on echo cancellation, noise suppression and automatic gain on the microphone (in a browser:
getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } })).
- Use them once, within 30 seconds (
expiresAt). For a new session, call reconnect. - One join at a time. A new join replaces the team member's current connection on the call.
Keep them secret
Never write credentials to a log.
Errors
Every error is { status, reason, message }. Some have no reason; use the HTTP status and the message. The errors below apply to every route; each endpoint's page lists its own.
Checked first, on every route
| Status | reason | Message | When |
|---|---|---|---|
| 401 | UNAUTHORIZED | Missing authorization token · Invalid Public API key | The API key. |
| 400 | BAD_REQUEST | The field's error · whatsappNumber is only accepted when channel is PSTN · agentNumber must be a phone number with its country code | A field is missing, malformed or not allowed. |
| 400 | MODE_NOT_SUPPORTED | VOIP calls on PSTN are not supported yet. · DIRECT calls on WHATSAPP are not supported yet. | A channel and mode pair not available yet. |
| 400 | BAD_REQUEST | No team member found for this phone in your organization · Multiple users match this phone; contact support | agentNumber matches no active team member, or more than one. |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests, please try again later. | See Rate limits. Retry-After says when to retry. |
Rate Limits
| Route | Limit | Counted per |
|---|---|---|
POST /v1/call/person | 60 a minute. A PSTN call also counts in /v1/call/agent's 10 a minute. | Organization |
/updates, /accept, /decline, /reconnect, /end | 60 a minute each | Organization and team member |
GET /v1/call/person/{callId} | 300 a minute | Organization |
- Limited replies carry
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset; a429carriesRetry-After. - On top of these, every public route allows at most 100 requests a second from one IP address.
Moving from the Old Routes
The /whatsapp/call/* and /v1/call/create routes are for browser web calling and are being retired. On numbers where they are no longer available they answer 400 Calling is not supported on this number. Use /v1/call/person instead:
| Old route | New route |
|---|---|
POST /whatsapp/call/create (from, to, sdp) | POST /v1/call/person (channel: WHATSAPP, mode: VOIP, from, to, agentNumber) |
POST /whatsapp/call/updates | POST /v1/call/person/updates (agentNumber) |
POST /whatsapp/call/accept (sdp) | POST /v1/call/person/{callId}/accept (agentNumber) |
POST /whatsapp/call/decline | POST /v1/call/person/{callId}/decline, or /end to hang up |
POST /whatsapp/call/reconnect | POST /v1/call/person/{callId}/reconnect |
POST /whatsapp/call/ping | Not needed: the audio connection's keep-alive replaces it. |
- The new routes send no SDP: your app joins with the returned credentials instead (see Joining a VOIP call).
Updated about 2 hours ago
What’s Next
Place your first call, or set a team member up to take incoming WhatsApp calls.

