New Safe Mode warms up new numbers to cut the risk of a WhatsApp ban. Learn more →

Maytapi/Blog/Guides

WhatsApp Call Webhooks: Missed, Rejected and Ringing Calls via API

Receive WhatsApp call events on your webhook, read call history with getCalls, reject or start calls, and auto-reply to missed calls from your CRM.

Guides Updated Oct 27, 2026·7 min read

WhatsApp Call Webhooks: Missed, Rejected and Ringing Calls via API

Calls are the part of WhatsApp that most integrations ignore. A customer rings your business number, nobody picks up, and the only trace is a missed-call line on someone's phone. With the Maytapi API, every call on your connected number becomes a webhook event and an entry in a call history you can query. This guide covers the call events you receive, what their payloads contain, the call endpoints (getCalls, call/reject, call/start, call/createCallLink), and a working pattern that logs missed calls to a CRM and answers the caller with a WhatsApp message.

What call events you receive

Call events arrive on your webhook as ordinary message events. The message.type is info and the message.subtype tells you what happened:

Subtype When it is sent
call/ringing A call starts ringing on your number
call/miss The call ended without being answered
call/reject The call was declined
call/accept The call was answered on another device, such as the phone itself
Lifecycle of a WhatsApp call as webhook events: call/ringing while it rings, then one of call/miss, call/reject or call/accept when it ends

There are two kinds of event here. call/ringing is a live notification: it fires while the call is still ringing, so you can react before it ends. The other three are call log entries that WhatsApp writes when the call is over, and they reach you the same way messages do.

A note on call/accept: your API connection works as a linked device and cannot answer a call itself. When you see call/accept, someone took the call on the phone or another device linked to the number.

Each event also carries isVideo, so you can tell voice calls from video calls. WhatsApp does not report call duration in these events, and there are no separate "initiated" or "answered" stages beyond the four subtypes above.

The call webhook payload

All call events use the standard webhook envelope described in the webhook events guide. A call/ringing event looks like this:

JSON
{
  "type": "message",
  "product_id": "1a2b3c4d-...",
  "phone_id": 12345,
  "user": { "id": "1415xxxxx67@c.us", "name": "John Doe", "phone": "1415xxxxx67" },
  "conversation": "1415xxxxx67@c.us",
  "conversation_name": "John Doe",
  "receiver": "1415xxxxx21",
  "timestamp": 1704702074,
  "message": {
    "id": "001FD1B8FA609DB8ED33F441A777AF7B",
    "_serialized": "001FD1B8FA609DB8ED33F441A777AF7B",
    "type": "info",
    "subtype": "call/ringing",
    "fromMe": false,
    "isVideo": false
  },
  "reply": "https://api.maytapi.com/api/1a2b3c4d-.../12345/sendMessage"
}

The fields you will actually use:

  • user is the caller, with id, name and phone. Privacy ids (@lid) are resolved to a phone number when WhatsApp makes one available.
  • conversation is the chat the call belongs to. For one-to-one calls it is the caller's @c.us id.
  • message.fromMe is true when the call was made from your own number, for example from the phone. Filter on it so you only act on incoming calls.
  • message.isVideo separates voice and video calls.
  • The call id. Some connections include a separate message.callId with the raw call id and put a full message id in message.id. Read message.callId first and fall back to message.id. That is the value call/reject and getCalls/{callId} expect. To see which form your connection uses, log the first call/ringing event you receive.

A call/miss, call/reject or call/accept event has the same envelope. Its message carries type, subtype, id, _serialized and fromMe:

JSON
{
  "type": "info",
  "subtype": "call/miss",
  "id": "false_1415xxxxx67@c.us_3EB0C7A1B2D3E4F5A6B7",
  "_serialized": "false_1415xxxxx67@c.us_3EB0C7A1B2D3E4F5A6B7",
  "fromMe": false,
  "isVideo": false
}

isVideo is included on most connections for these three subtypes as well, but it is not guaranteed on every connection, so treat a missing value as unknown rather than as a voice call. The id here is the call log message id. Don't use it with call/reject.

Note: Call events reach you only after a webhook is set. Configure it once with setWebhook or in the console. Delivery receipts (acks) are off by default and only arrive once you enable them with setAckPreference, so a call-only handler sees just messages and events.

Read call history with getCalls

Webhooks can be missed while your server is down, so the API also keeps a call history. GET /getCalls lists finished calls:

Terminal
curl "https://api.maytapi.com/api/$PRODUCT_ID/$PHONE_ID/getCalls?page=1" \
  -H "x-maytapi-key: $TOKEN"
JSON
{
  "success": true,
  "limit": 500,
  "count": 1,
  "total": 1,
  "page": 1,
  "data": [
    {
      "timestamp": 1704702000,
      "uid": "1415xxxxx67@c.us",
      "fromMe": false,
      "conversation": "1415xxxxx67@c.us",
      "user": { "id": "1415xxxxx67@c.us", "name": "John Doe", "phone": "1415xxxxx67" },
      "message": { "id": "...", "_serialized": "...", "type": "info", "subtype": "call/miss", "isVideo": false }
    }
  ]
}

Each item uses the same shape as the webhook: user is who made the call (you, when fromMe is true) and conversation is the other party, or the group id for a group call.

Three query parameters control the list:

Parameter Effect
full true adds full contact details (name, phone, image) to user and lowers the page size from 500 to 100
page Page number, starting at 1
sort true returns oldest first; the default is newest first

The history only contains the three outcomes call/miss, call/reject and call/accept. Calls still ringing are not listed. It is also limited to the most recent calls the connected session can see, up to 5000. Calls from before you paired the number may be missing.

Look up a single call

GET /getCalls/{callId} returns one call in the same item shape. While the call is still ringing, the subtype is call/ringing. Once it has ended, you get the final outcome:

Terminal
curl "https://api.maytapi.com/api/$PRODUCT_ID/$PHONE_ID/getCalls/001FD1B8FA609DB8ED33F441A777AF7B" \
  -H "x-maytapi-key: $TOKEN"

Use the call id from the call/ringing event. When the call cannot be found, data is null. Check data rather than success, because success is not the same on every connection in the not-found case.

Reject, start and share calls

Reject an incoming call

POST /call/reject declines a call that is still ringing. It takes the call id from the call/ringing webhook:

Terminal
curl -X POST "https://api.maytapi.com/api/$PRODUCT_ID/$PHONE_ID/call/reject" \
  -H "x-maytapi-key: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"callId": "001FD1B8FA609DB8ED33F441A777AF7B"}'

Rejecting only works while the call rings. After it ends there is nothing left to reject, and the request returns success: false with a message.

Start an outgoing call

POST /call/start rings a number from your account. This is an attention call: it rings the recipient and hangs up as soon as they answer or when duration runs out. It carries no audio, so use it as a nudge before an important message, not as a phone line.

Terminal
curl -X POST "https://api.maytapi.com/api/$PRODUCT_ID/$PHONE_ID/call/start" \
  -H "x-maytapi-key: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to_number": "1415xxxxx67", "duration": 5}'

to_number is the number with country code and no +. Always send duration: it must be a whole number of seconds from 1 to 30, or the call is not started. A successful response returns data.callId and data.chatId. On failure you get success: false with a message and a code, for example when the number is not on WhatsApp or another call is already active.

POST /call/createCallLink returns a shareable https://call.whatsapp.com/... link that people can use to join a call:

Terminal
curl -X POST "https://api.maytapi.com/api/$PRODUCT_ID/$PHONE_ID/call/createCallLink" \
  -H "x-maytapi-key: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"callType": "video", "timestamp": 1893456000}'

Send callType and a timestamp (Unix seconds, in the future) for the scheduled call. The link comes back in data. You can send it in a normal text message, for example in a booking confirmation.

Log missed calls to a CRM and reply automatically

Here is the use case most teams want: when a customer calls and nobody answers, create a CRM activity and send them a WhatsApp message so the conversation keeps going.

Missed-call automation: webhook receives call event, filters incoming one-to-one calls, rejects after hours, logs to CRM and sends one WhatsApp reply

The handler below runs on Express and Node.js 18 or newer. It does two things:

  1. On call/ringing during closed hours, it rejects the call so the caller is not left waiting, and follows up right away.
  2. On call/miss or call/reject from a customer, it logs the call and sends one reply.
JavaScript
import express from "express";

const { PRODUCT_ID, PHONE_ID, TOKEN } = process.env;
const base = `https://api.maytapi.com/api/${PRODUCT_ID}/${PHONE_ID}`;
const headers = { "x-maytapi-key": TOKEN, "Content-Type": "application/json" };

const app = express();
app.use(express.json());

const lastFollowUp = new Map(); // conversation -> time of last follow-up (ms)
const isOpen = () => { const h = new Date().getHours(); return h >= 9 && h < 18; };

async function followUp(event, outcome) {
  const last = lastFollowUp.get(event.conversation) ?? 0;
  if (Date.now() - last < 6 * 60 * 60 * 1000) return; // once per 6 hours per chat
  lastFollowUp.set(event.conversation, Date.now());

  await crm.logCall({
    phone: event.user?.phone,
    name: event.user?.name,
    outcome,
    video: event.message.isVideo,
    at: new Date(event.timestamp * 1000)
  });

  const text = isOpen()
    ? "Sorry we missed your call. Reply here and we'll answer you in a few minutes."
    : "Thanks for calling. We're closed right now and open again at 9:00. Leave us a message here.";

  await fetch(`${base}/sendMessage`, {
    method: "POST",
    headers,
    body: JSON.stringify({ to_number: event.conversation, type: "text", message: text })
  });
}

app.post("/webhook", async (req, res) => {
  res.sendStatus(200); // acknowledge first, work after
  const event = req.body;
  const m = event.message;
  if (event.type !== "message" || m?.type !== "info") return;
  if (!m.subtype?.startsWith("call/") || m.fromMe) return;
  if (event.conversation?.endsWith("@g.us")) return; // skip group calls

  if (m.subtype === "call/ringing" && !isOpen()) {
    const callId = m.callId || m.id;
    await fetch(`${base}/call/reject`, { method: "POST", headers, body: JSON.stringify({ callId }) });
    return followUp(event, "reject");
  }

  if (m.subtype === "call/miss" || m.subtype === "call/reject") {
    return followUp(event, m.subtype.split("/")[1]);
  }
});

app.listen(3000);

crm.logCall stands for your own CRM client. A few details are worth keeping:

  • One follow-up per caller. A call you reject can be followed by a call/reject log event, and a caller who tries three times in a row produces three events. The per-chat window turns all of that into one CRM entry and one message. Keep it in Redis or your database if you run more than one instance.
  • Resolve @lid ids. If conversation ends in @lid, WhatsApp did not share the number. The LID to phone number guide shows how to resolve it before you match the CRM record.
  • Reconcile from history. A scheduled job that reads getCalls and compares it with the CRM catches any calls you missed while your server was offline.

Practical tips

  • Answer only people who called you. A short reply to a missed call is expected. Sending automated messages to numbers that never contacted you is a different matter. Read how Safe Mode prevents WhatsApp bans before you add any outbound flow.
  • Use call/start sparingly. An unexpected call is more intrusive than a message, and automated calls to people who don't know your number are the kind of activity that gets a number restricted. Keep it for people who asked to be called, such as an appointment reminder, and read the Safe Mode and ban prevention guide first.
  • Label callers. If your CRM tracks stages, combine this with the labels sync guide to tag chats that came in by phone.
  • Hand off to agents. On a shared number, route missed-call replies into the multi-agent platform so a person picks up the thread.

Next steps

Published Sep 15, 2026 · Updated Oct 27, 2026 ← All posts
Start using the WhatsApp API

Get your key, send your first message in ten minutes

No approval, no templates, no card. Every endpoint is open for three days.