Zernio
APIFor you
Zernio
APIFor you
Platforms

WhatsApp

Overview

Set up

Connection & setupPhone NumbersSandbox

Messaging

InboxTemplatesBroadcastsFlowsGroup chatsContactsMeta Business Agent

Calls

Calling

Ad types

Click-to-WhatsApp ads

Reference

Pricing & costsMedia & limits
Dashboard
OverviewPlatformsAPI ReferenceResources
WhatsApp

Calling

Turn on WhatsApp Business Calling for a connected number, forward calls to a phone, SIP endpoint or AI voice agent, and place outbound calls with the customer's permission.


By the end of this page, customers can call your WhatsApp number from inside the chat, and the call rings wherever you chose. You can also place calls to customers who allowed it.

Where calls go

Zernio bridges each call to a forwardTo destination:

  • a phone number (tel:)
  • a SIP endpoint such as a Vapi, Retell or ElevenLabs agent (sip:)
  • a WebSocket media server of your own (wss:)

Calling runs on /v1/phone-numbers/{id}/whatsapp/calling. The /v1/whatsapp/phone-numbers/{id}/calling paths are deprecated aliases.

What it costs

Both directions meter the carrier connection per minute, plus $0.004/minute when recording is on.

Meta charges its own per-minute rate on outbound calls only, billed directly to your WABA, and nothing on inbound (WhatsApp rates).

GET /v1/whatsapp/calls/estimate with accountId, to and minutes returns the outbound legs before you dial.

Before you start

You need a connected WhatsApp account with usage-based billing active.

Meta blocks business-initiated calls on numbers registered in the US, Canada, Egypt, Vietnam and Nigeria; inbound calls to those numbers still work.

Elsewhere, POST /v1/phone-numbers/{id}/whatsapp/calling returns 422 in two cases:

  • The team is not on usage-based billing.
  • The number's messaging limit is below Meta's ~2,000-daily-recipient threshold.

A new number starts at TIER_250. Send messages from it until Meta raises the limit, then enable calling.

Step 1: Read the calling config

Call GET /v1/whatsapp/calling with accountId. The response carries phoneNumberDocId, the {id} every other calling call takes, plus the current destination and recording flag.

import Zernio from '@zernio/node';

const zernio = new Zernio();
const accountId = '66b2e19d8c3f5a7e9d0b1c2d';

const { data: config } = await zernio.whatsappcalling.getWhatsAppCallingConfig({
  query: { accountId }
});

const phoneNumberId = config.phoneNumberDocId;

Response (200):

{
  "phoneNumberDocId": "66f6c3d4e5f6a7b8c9d0e1f3",
  "phoneNumber": "+14155550100",
  "callingEnabled": false,
  "callDeepLink": null,
  "forwardTo": null,
  "recordingEnabled": false,
  "sipAuthUsername": null,
  "sipAuthPasswordConfigured": false,
  "callerIdMode": "business",
  "callerIdVerified": true,
  "maxCallDurationSeconds": null,
  "forwardCallerId": "business"
}

Step 2: Enable calling

Call POST /v1/phone-numbers/{id}/whatsapp/calling with accountId and forwardTo. Zernio sets calling.status=ENABLED on Meta, stores the Meta-issued SIP password encrypted, and snapshots the destination.

const { data: enabled } = await zernio.whatsappcalling.enableWhatsAppCalling({
  path: { id: phoneNumberId },
  body: {
    accountId,
    forwardTo: 'tel:+13105559876',
    recordingEnabled: false
  }
});

console.log(enabled.callingEnabled);

Response (200):

{
  "success": true,
  "callingEnabled": true,
  "forwardTo": "tel:+13105559876",
  "callerIdMode": "business"
}

Optional settings:

  • Recording is off by default. With recordingEnabled: true a consent prompt plays before recording starts and the per-minute surcharge applies.
  • maxCallDurationSeconds caps a forwarded call, so a destination that never hangs up cannot bill dead air.
  • forwardCallerId: "caller" presents the WhatsApp user's number to a sip: destination instead of yours.

After enabling, callDeepLink on the config is a public https://wa.me/call/... link. It starts a WhatsApp call to the number from a website, email or QR code.

Change the destination or the recording flag later with PATCH /v1/phone-numbers/{id}/whatsapp/calling and the same body shape.

Turn calling off with DELETE /v1/phone-numbers/{id}/whatsapp/calling?accountId=.... Disabling keeps forwardTo and the SIP credentials, so re-enabling needs no reconfiguration.

Step 3: Check call permission

WhatsApp requires the customer to allow calls before you dial them. Call GET /v1/whatsapp/call-permissions with accountId and to.

const { data: permission } = await zernio.whatsappcalling.getWhatsAppCallPermissions({
  query: { accountId, to: '+13105551234' }
});

console.log(permission.permission.status);

Response (200):

{
  "permission": { "status": "no_permission" },
  "actions": [
    { "action_name": "send_call_permission_request", "can_perform_action": true },
    { "action_name": "start_call", "can_perform_action": false }
  ]
}

status is permanent, temporary (with expiration_time in Unix seconds) or no_permission.

When start_call is not allowed, send the consent prompt with POST /v1/whatsapp/calls and action: "send_call_permission_request" (optionally bodyText). The customer taps Allow in WhatsApp.

Meta limits the prompt to 1 per customer per 24 hours and 2 per 7 days. It needs an open 24-hour service window.

Step 4: Place an outbound call

Call POST /v1/whatsapp/calls with accountId and to. The call bridges to the number's forwardTo unless you pass a per-call forwardTo override.

Send an Idempotency-Key header so a retry replays the response instead of dialing, and billing, a second call.

const { data: call } = await zernio.whatsappcalling.initiateWhatsAppCall({
  body: { accountId, to: '+13105551234' }
});

console.log(call.callId, call.status);

Response (200):

{
  "success": true,
  "callId": "66f8e5f6a7b8c9d0e1f2a3b5",
  "status": "dialing",
  "direction": "outbound",
  "to": "+13105551234",
  "forwardTo": "tel:+13105559876",
  "recordingEnabled": false
}

The rest of the call's life arrives on the call webhooks.

Hang up with POST /v1/voice/calls/{callId}/end, the same endpoint as voice calls.

It ends the call whether it is still ringing or already answered. It works for outbound calls and for inbound ones (call.id on the call.received webhook).

A call forwarded to a wss:// destination also ends when your WebSocket closes. Dropping the socket stops the call and its billing.

curl -X POST "https://zernio.com/api/v1/voice/calls/66f8e5f6a7b8c9d0e1f2a3b5/end" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

Step 5: Read call history

Call GET /v1/whatsapp/calls with accountId, optionally filtered by status, direction, since and until.

Each call carries its duration, end reason, recording URL when recording was on, and a billing breakdown.

Pass the returned nextCursor as before for the next page.

const { data: history } = await zernio.whatsappcalling.listWhatsAppCalls({
  query: { accountId, direction: 'inbound', limit: 50 }
});

for (const c of history.calls) {
  console.log(c.direction, c.status, c.durationSeconds, c.billing?.billableCostUSD);
}

Response (200):

{
  "calls": [
    {
      "_id": "66f8e5f6a7b8c9d0e1f2a3b5",
      "direction": "inbound",
      "from": "+13105551234",
      "to": "+14155550100",
      "status": "ended",
      "startedAt": "2027-01-01T09:00:00Z",
      "endedAt": "2027-01-01T09:04:12Z",
      "durationSeconds": 252,
      "endReason": "hangup",
      "billing": {
        "metaCostUSD": 0,
        "telnyxCostUSD": 0.0336,
        "recordingCostUSD": 0,
        "billableCostUSD": 0.0672,
        "totalCostUSD": 0.0672,
        "currency": "USD"
      }
    }
  ],
  "nextCursor": null
}
FieldMeaning
billing.telnyxCostUSD, billing.recordingCostUSDThe raw upstream costs
billing.billableCostUSDWhat Zernio bills for them, at the published call rate
billing.metaCostUSDMeta's per-minute fee, billed by Meta directly to your WABA and shown for reference only

Inbound calls carry no Meta fee, which is why metaCostUSD is 0 here.

If it fails

A 409 from POST /v1/whatsapp/calls means the customer has not allowed calls:

{
  "error": "No active call permission. Send a permission request first."
}

Run Step 3, send the permission request, and dial once start_call is allowed.

A 422 means one of:

  • calling is not enabled on the number
  • the number's country blocks business-initiated calls
  • the Meta SIP credentials are missing

A 502 means the carrier could not originate the call, and it was marked failed.

Every error uses the envelope in error handling.

Related

  • WhatsApp rates: Meta's per-minute rate by country and the recording surcharge.
  • Calling API: every endpoint, including caller-ID verification and recordings.
  • Call webhooks: ringing, answered, ended and failed events.
  • Voice and calls: PSTN calling on the same numbers, and the AI agent integrations.
  • Phone numbers: which numbers can carry calling.
Was this page helpful?

Meta Business Agent

Provision and operate Meta's AI agent on a WhatsApp number by API, so the merchant never opens Business Manager

Click-to-WhatsApp ads

Capture the click id Meta attaches to a Click-to-WhatsApp conversation and send conversion events back to Meta so they attribute to the ad.

On this page

Where calls goWhat it costsBefore you startStep 1: Read the calling configStep 2: Enable callingStep 3: Check call permissionStep 4: Place an outbound callStep 5: Read call historyIf it failsRelated