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

channelmodeWhat happensStatus
WHATSAPPVOIPThe team member talks from your app (browser, server or mobile), using the credentials the API returns.Available
PSTNDIRECTThe team member's own phone rings first; when they pick up, the customer is dialled and the two are connected.Available
PSTNVOIP—Not yet: 400 MODE_NOT_SUPPORTED
WHATSAPPDIRECT—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 Authorization header, as it is, with no Bearer in front. A missing or wrong key gets 401.
  • 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": "..." }
  • data is always the call, read fresh (see The call object). A call is the same object on every route.
  • credentials come only on a VOIP call out, accept and reconnect (see Joining a VOIP call).

Endpoints

EndpointBodyWhat it does
POST /v1/call/personsee Call a CustomerCall a customer.
POST /v1/call/person/updatesagentNumberKeeps 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}/acceptagentNumber, mode (optional, VOIP)Answer a WhatsApp call ringing for the team member.
POST /v1/call/person/{callId}/declineagentNumberRefuse a call ringing for the team member. It ends for everyone it rang.
POST /v1/call/person/{callId}/reconnectagentNumberNew credentials after the team member's connection dropped. Within 60 seconds of the drop.
POST /v1/call/person/{callId}/endagentNumberHang 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

  1. POST /v1/call/person {channel: WHATSAPP, mode: VOIP, from, to, agentNumber}
  2. join with the credentials (see Joining a VOIP call) → the customer's WhatsApp rings
  3. the customer answers → talking
  4. POST /v1/call/person/{callId}/end → close the audio connection

A customer calls on WhatsApp and a team member answers

  1. POST /v1/call/person/updates {agentNumber}, every 2 s → a call appears, callStatus INITIATED, callDirection INCOMING
  2. POST /v1/call/person/{callId}/accept {agentNumber} → credentials
  3. join with the credentials → talking (or POST …/decline to refuse it)
  4. POST /v1/call/person/{callId}/end → close the audio connection

A team member calls a customer on a phone line

  1. POST /v1/call/person {channel: PSTN, mode: DIRECT, from, to, agentNumber}
  2. the team member's phone rings → they pick up → the customer is dialled → connected
  3. 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.

FieldWhat it holds
callIdThe call's id.
channel, modeWHATSAPP · VOIP, or PSTN · DIRECT.
callDirectionINCOMING or OUTGOING.
callStatusINITIATED, IN_PROGRESS, END, MISSED, REJECT, FAILED.
from, toSet by direction, as in the webhooks.
agentNumberThe team member who took, is on, or declined the call; null before anyone has.
customerName, customerNumber, customerUserIdThe customer. customerUserId is the WhatsApp user id when WhatsApp does not share the caller's number.
whatsappNumber, chatIdThe chat the call is logged in. whatsappNumber is set on PSTN calls only.
ringExpiresAtFor an incoming WhatsApp call that can still be accepted: until when. Otherwise null.
callStartedAt, callEndedAt, durationInSeconds, recordingUrl, failedReasonFilled 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.

  1. Open a WebSocket to wssUrl. Do not register.
  2. Send one call (INVITE) to sipUri. Add every header in headers, and answer the login challenge with username and password.
  3. Audio only. Offer the codecs in the order given, Opus first.
  4. Use ICE with DTLS-SRTP and rtcp-mux, and the iceServers exactly as given (the list may be empty).
  5. Send a keep-alive every 20–30 seconds while the call is up.
  6. 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

StatusreasonMessageWhen
401UNAUTHORIZEDMissing authorization token · Invalid Public API keyThe API key.
400BAD_REQUESTThe field's error · whatsappNumber is only accepted when channel is PSTN · agentNumber must be a phone number with its country codeA field is missing, malformed or not allowed.
400MODE_NOT_SUPPORTEDVOIP calls on PSTN are not supported yet. · DIRECT calls on WHATSAPP are not supported yet.A channel and mode pair not available yet.
400BAD_REQUESTNo team member found for this phone in your organization · Multiple users match this phone; contact supportagentNumber matches no active team member, or more than one.
429RATE_LIMIT_EXCEEDEDToo many requests, please try again later.See Rate limits. Retry-After says when to retry.

Rate Limits

RouteLimitCounted per
POST /v1/call/person60 a minute. A PSTN call also counts in /v1/call/agent's 10 a minute.Organization
/updates, /accept, /decline, /reconnect, /end60 a minute eachOrganization and team member
GET /v1/call/person/{callId}300 a minuteOrganization
  • Limited replies carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 carries Retry-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 routeNew route
POST /whatsapp/call/create (from, to, sdp)POST /v1/call/person (channel: WHATSAPP, mode: VOIP, from, to, agentNumber)
POST /whatsapp/call/updatesPOST /v1/call/person/updates (agentNumber)
POST /whatsapp/call/accept (sdp)POST /v1/call/person/{callId}/accept (agentNumber)
POST /whatsapp/call/declinePOST /v1/call/person/{callId}/decline, or /end to hang up
POST /whatsapp/call/reconnectPOST /v1/call/person/{callId}/reconnect
POST /whatsapp/call/pingNot 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).

What’s Next

Place your first call, or set a team member up to take incoming WhatsApp calls.

Did this page help you?