Yes, you can make and answer WhatsApp calls through an API. Link a WhatsApp number to Mossmoon with one QR scan, call POST /lines/:id/call to ring a contact, accept incoming calls from a webhook, and stream the live audio to an agent’s browser or to an AI voice agent.
Below are the real endpoints, payloads and limits. Calling is on the $20 per line plan, and every calling feature is listed in the calling features.
Is there a WhatsApp calling API?
There are two ways to get WhatsApp calls into software. Meta’s WhatsApp Business API has a calling feature for numbers on its Cloud API. Calls the business places are billed by length and need each person’s permission first, business numbers in the US and Canada can’t place them, and the number has to run on the Cloud API rather than the WhatsApp Business app (see Meta’s calling docs).
Mossmoon takes the other route. It links your customer’s own number as a linked device, and calls go out from that number like any WhatsApp call. It works on personal WhatsApp and the Business app, with no templates, business verification or per-minute fees. The Business API vs personal API explainer covers the wider choice.
How do I make a WhatsApp call from my CRM?
Five steps take a linked number to a live call. Your server does the API calls (your key must never reach a browser), and the page or a backend joins the audio.
- 1
Put the line on the calling plan
Create the line withcalling_enabled: true, or switch an existing one with aPATCH. Nobody has to scan the QR again. Without it, the call endpoints answer403 calling_disabled.Switch a line to callingPATCH /api/v1/wa/lines/wa_8f3c2e1a Authorization: Bearer mm_live_... Content-Type: application/json { "calling_enabled": true } → 200 OK { "line_id": "wa_8f3c2e1a", "status": "ready", "calling_enabled": true, "monthly_price_cents": 2000, ... } - 2
Check WhatsApp has calling on for the account
Calling on linked devices is a WhatsApp feature Meta has been rolling out account by account. Your customer can check in ten seconds: open WhatsApp on a computer, click into any chat and look for the phone icon in the header. No icon means403 calling_unavailablefor now. - 3
Place the call
POST /lines/:id/callwithto(a number in E.164) orto_wa_id. The answer is202while it rings, with anaudio_relay_urlfor the call’s sound.curl https://mossmoon.app/api/v1/wa/lines/wa_8f3c2e1a/call \ -H "Authorization: Bearer mm_live_..." \ -H "Content-Type: application/json" \ -d '{ "to": "+15709302189" }'const res = await fetch("https://mossmoon.app/api/v1/wa/lines/wa_8f3c2e1a/call", { method: "POST", headers: { Authorization: `Bearer ${process.env.MOSSMOON_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ to: "+15709302189" }), }); const call = await res.json(); // 202: status "ringing" + audio_relay_urlres = requests.post( "https://mossmoon.app/api/v1/wa/lines/wa_8f3c2e1a/call", headers={"Authorization": f"Bearer {MOSSMOON_KEY}"}, json={"to": "+15709302189"}, ) call = res.json() # 202: status "ringing" + audio_relay_url202 Accepted { "call_id": "wacall_3a4b5c6d", "line_id": "wa_8f3c2e1a", "direction": "outbound", "status": "ringing", "to_e164": "+15709302189", "to_wa_id": "[email protected]", "audio_relay_url": "wss://wa-host.mossmoon.app/calls/wa_8f3c2e1a/wacall_3a4b5c6d/audio?token=...", "audio_format": { "encoding": "pcm_s16le", "sample_rate": 16000, "channels": 1 }, "expires_at": "2026-10-01T21:20:33.000Z", "created_at": "2026-10-01T19:20:33.456Z", "answered_at": null, "ended_at": null, "duration_seconds": null, "ended_by": null, "failure_reason": null } - 4
Connect the audio
Handaudio_relay_urlto the browser SDK so an agent talks with their own mic and speakers, or open it from your server for an AI voice agent. Both are covered below. - 5
Follow the call with webhooks
call.ringing,call.answered,call.endedandcall.failedarrive on the line’s webhook. To end the call from your side,POST /call/:call_id/hangup.
Two errors are worth handling up front. 409 call_in_progress means the line already has a call (its call_id comes back with it), and 409 line_not_ready means the phone is disconnected, so wait for line.ready. Every call error is safe to retry because none of them start a call. The full list is under call errors.
How do I answer or decline incoming WhatsApp calls?
When someone calls the line, you get call.ringing with direction: "inbound", the caller’s number, an accept_url and a reject_url. Show it in your UI however you like.
POST https://your-app.com/webhooks/whatsapp
X-Mossmoon-Event: call.ringing
X-Mossmoon-Signature: sha256=...
{
"event": "call.ringing",
"line_id": "wa_8f3c2e1a",
"data": {
"call_id": "wacall_inb_9f8e7d6c",
"direction": "inbound",
"from_e164": "+15559998888",
"from_wa_id": "[email protected]",
"expires_at": "2026-10-01T19:25:45.000Z",
"accept_url": "https://mossmoon.app/api/v1/wa/lines/…/call/wacall_inb_9f8e7d6c/accept",
"reject_url": "https://mossmoon.app/api/v1/wa/lines/…/call/wacall_inb_9f8e7d6c/reject"
}
}POST the accept URL with your API key before expires_at. The response carries the audio_relay_url, and you connect to it exactly as for an outbound call.
POST /api/v1/wa/lines/wa_8f3c2e1a/call/wacall_inb_9f8e7d6c/accept
Authorization: Bearer mm_live_...
→ 200 OK
{ "call_id": "wacall_inb_9f8e7d6c", "status": "ringing",
"audio_relay_url": "wss://wa-host.mossmoon.app/calls/...", ... }- Decline with the
reject_url. The caller sees the call declined. - Do nothing and it ends as
missed. - Too late? If the caller hung up first, accept returns
409 not_ringing.
Only one-to-one voice calls ring through. Video and group calls to the line are not passed on. See inbound calls in the docs.
How does an agent talk on the call from the browser?
Load our small browser SDK, mossmoon-calls.js, and pass it the audio_relay_url. It connects the agent’s microphone and speakers to the call, inside your page, with no new tab or window.
<script src="https://mossmoon.app/sdk/mossmoon-calls.js"></script>
<script>
callButton.onclick = async () => {
// 1. Microphone first, inside the click. If the agent clicks "Block",
// stop here: nobody's phone rings with silence on the line.
let mic;
try { mic = await MossmoonCalls.requestMicrophone(); }
catch (e) { return showHelp(e.code); }
// e.code: microphone_denied, no_microphone, insecure_context
// 2. Your server calls POST /call (or .../accept for an inbound call)
// and hands audio_relay_url back to the page.
const res = await fetch("/my-api/call", { method: "POST" });
const { audio_relay_url } = await res.json();
// 3. Join the audio.
const call = await MossmoonCalls.connect(audio_relay_url, {
stream: mic,
onStatus: (c) => console.log(c.status), // ringing, answered, ended...
onEnded: (c) => console.log("call over", c),
});
muteButton.onclick = () => call.mute(true); // mic off, call stays up
};
</script>Ask for the microphone before placing the call, as above, so a blocked mic never leaves someone’s phone ringing with nobody there. The browser asks once and remembers. A few things to know:
- The page must be served over https. If your CRM embeds it in an iframe, add
allow="microphone". - The SDK reconnects by itself after a brief network drop, and a headset avoids echo on laptop speakers.
call.disconnect()only stops the audio. To end the WhatsApp call, use the hangup endpoint.
Details are under call audio.
Can an AI voice agent make or answer WhatsApp calls?
Yes. audio_relay_url is a plain WebSocket, signed for that one call and valid for two hours. There is no SDP or ICE to negotiate, so any WebSocket client on your server can join it and both hear and speak.
- Audio frames
- Binary, both directions: PCM16 little-endian, mono, 16 kHz
- Frame size
- Any size; 20 to 40 ms works well (20 ms is 640 bytes)
- Text frames
- JSON from the server:
{"type":"hello"|"status","call":{...}} - Sockets per call
- Up to three. Each one hears the other party
- Closing a socket
- Stops its audio. It does not end the call
import WebSocket from "ws";
// From POST /call (outbound) or POST .../accept (inbound).
const ws = new WebSocket(audio_relay_url);
const FINAL = ["ended", "declined", "missed", "failed"];
ws.on("message", (data, isBinary) => {
if (!isBinary) {
// JSON text frames: { "type": "hello" | "status", "call": { ... } }
const msg = JSON.parse(data.toString());
if (FINAL.includes(msg.call?.status)) ws.close();
return;
}
// The caller's voice: PCM16 little-endian, mono, 16 kHz.
voiceModel.sendAudio(data);
});
// Your agent's reply, in the same format. 20 ms of audio = 640 bytes.
voiceModel.on("audio", (pcm16) => ws.send(pcm16));If your voice model works at another sample rate, resample on your side. For an AI receptionist, accept the call from the call.ringing webhook handler and open the socket straight away, so the caller hears your agent within a moment of the call connecting.
Which call webhooks will I get?
Calls fire signed webhooks on the line’s webhook_url, with the same envelope and signature as messages. If you haven’t set up an endpoint yet, start with the webhook setup guide.
{ "event": "call.answered",
"data": { "call_id": "wacall_3a4b5c6d", "direction": "outbound",
"answered_at": "2026-10-01T19:20:41.234Z" } }
{ "event": "call.ended",
"data": { "call_id": "wacall_3a4b5c6d", "direction": "outbound",
"ended_at": "2026-10-01T19:24:11.789Z", "duration_seconds": 210,
"ended_by": "recipient" } }
// ended_by: "agency" | "recipient" | "caller" | "network"
{ "event": "call.failed",
"data": { "call_id": "wacall_3a4b5c6d", "direction": "outbound",
"reason": "missed" } }
// reason: "missed" | "declined" | "canceled" | "network_error"ended_by tells you who hung up: agency (you, through the API), recipient, caller or network. Log duration_seconds on the contact’s timeline and you have a call history in your CRM. GET /call/:call_id returns a call’s state for 10 minutes after it ends; the webhooks are the permanent record. To hang up from your side:
POST /api/v1/wa/lines/wa_8f3c2e1a/call/wacall_3a4b5c6d/hangup
Authorization: Bearer mm_live_...
→ 200 OK
{ "call_id": "wacall_3a4b5c6d", "status": "answered", ... }
// call.ended follows a moment later with the duration.On an inbound call that is still ringing, hangup declines it. Payloads are under call webhook events.
Can I make WhatsApp calls without writing code?
Yes, in two ways:
- From the Mossmoon dashboard. Any connected line on the calling plan gets a Call button: type a number and talk with your computer’s mic and speakers.
- Call links.
POST /lines/:id/call-links { "type": "voice" }(orvideo) returns ahttps://call.whatsapp.com/…link to send in a chat. Whoever opens it waits in the call and the line’s phone gets a notification; someone joins from the phone. API calling can’t join call-link calls.
If a person in your product just wants to call from their own WhatsApp, a free wa.me click-to-call link is enough. Use the API when the call must come from the connected number, from a server, or with webhooks.
How much does WhatsApp calling cost?
$20 a line a month with calling, and the minutes are unlimited.
A messaging-only line is $15, so calling adds $5. Switching a line on in a period it already paid for charges the $5 difference right away; a line still in its free trial pays nothing now and its next charge is simply $20. Switching calling off takes effect at once and the line bills $15 from its next charge. The first 7 days after signup are free. See pricing and turning calling on for a line.
What are the limits of WhatsApp calling by API?
- One active call per line. That is WhatsApp’s own limit for one account. A second
POST /callgets409 call_in_progress, and a second inbound caller hears WhatsApp’s busy signal. Need parallel calls? Use more lines. - Voice, one to one. No video or group calls through the API.
- Depends on WhatsApp calling on linked devices. Most accounts have it now; some older or regional ones don’t yet, and get
403 calling_unavailable. - The line must be connected. If the phone has been logged out, calls fail with
409 line_not_readyuntil it relinks. - Your audio, your recording. Mossmoon carries the audio. For recordings or transcripts, save what your own socket receives and sends.
Mossmoon vs goghl.ai for WhatsApp calling
goghl.ai also links WhatsApp by QR, inside GoHighLevel. Its help center covers a missed-call trigger for workflows and a badge showing which channel a call came through, but not placing, answering or hearing calls. These are the calling features we offer that their public docs don’t list.
$20 a line with calling. First line free for 7 days.
| Feature | RecommendedMossmoon$20 a line with calling. First line free for 7 days.Start free → | goghl.ai |
|---|---|---|
| Place WhatsApp voice calls from the connected number | YesPOST /lines/:id/call, from your app or the dashboard | Not listedTheir docs cover a missed-call workflow trigger and a call channel badge |
| Answer or decline incoming calls in your own UI | Yesaccept_url and reject_url on call.ringing | Not listed |
| Talk from the browser with mic and speakers | Yesmossmoon-calls.js, with mute and auto-reconnect | Not listed |
| Live call audio for an AI voice agent | YesPCM16 16 kHz both ways over a WebSocket | Not listed |
| Webhooks when a call is answered or ends, with duration | Yescall.answered, call.ended with ended_by, call.failed with a reason | Not listedTheir webhooks are whatsapp.inbound, whatsapp.outbound and message.failed |
| Create WhatsApp call links | YesVoice or video, to send in a chat | Not listed |
Checked against goghl.ai’s public site and help center on October 16, 2026. “Not listed” means we couldn’t find it in their docs, not that it can’t exist. Only features goghl.ai doesn’t list are shown. Sources: missed-call trigger, call channel badge, help center index, goghl.ai.
The full three-way table, with Meta’s Business API, is on the features page.
Frequently asked questions
Yes, two kinds. Meta's Business API has calling for numbers on its Cloud API, where calls the business places are billed by length and need each person's permission first. Mossmoon lets software place and answer calls from any WhatsApp number linked by QR, personal or Business app, on a flat $20 per line a month.
Yes. Your server calls POST /api/v1/wa/lines/:id/call with the contact's number, and the browser SDK mossmoon-calls.js connects the agent's microphone and speakers inside your CRM page. The contact sees a normal WhatsApp call from the business's own number.
Yes. When someone calls the line you get a call.ringing webhook with an accept_url. POST to it and the response carries a WebSocket URL with the call's audio as raw PCM16, mono, 16 kHz in both directions, which any voice model or agent on your server can read and write.
On Mossmoon it is $20 per line a month instead of $15, with unlimited call minutes and messaging included. The first 7 days after signup are free. On Meta's Business API, calls the business places are billed by length instead.
Not yet. The Mossmoon GoHighLevel integration brings one-to-one WhatsApp chats into Conversations, but calls stay in the API and the Mossmoon dashboard for now.
No. API calls are one-to-one voice calls. You can create a voice or video call link to send in a chat, but someone joins that call from the phone.
WhatsApp hasn't turned on calling from linked devices for that account yet. If the phone icon is missing in a chat on WhatsApp on a computer, the line can't call. It starts working once Meta enables the account, with no new QR scan.
Ready to try it? Start free, link a number, switch on calling and place a test call from the dashboard.

