Answer and Place WhatsApp Calls With the API
Your instances can now pick up an incoming WhatsApp call and play a recording, turn it down with a text reply instead, or place a call themselves. Here is how it works, and what beta means for it.
Until now, a call to a number connected to WaAPI was something you could only watch happen: the call webhook fired, and the phone rang on into the void. Nobody was on the other end.
That changes with this release. A WaAPI instance can now answer an incoming call and play a recording into it, turn a call down and send a message instead, or place a call itself. It is on for every instance today, and it is in beta — the last section explains exactly what that means, including the one limit trial instances hit.
Three things you can do with it
Answer and play something. The instance picks up and plays an audio file into the call: opening hours, a queue notice, the sentence "please send this as a message and we will read it". On a video call it can show a still image or loop a short clip as the outgoing camera.
Turn the call down and reply in writing. The instance rejects the call and immediately sends a text to the caller. This is the one most businesses want. A missed call leaves the caller with nothing; a rejected call plus "we do not take calls here, but we read every message" moves them into the channel you actually serve.
Place a call yourself. Ring a number, optionally wait for the answer, and play audio once the call connects — a delivery notice, a two-factor prompt, an appointment reminder for people who do not read messages.
The part that needs no code
Two of the three work without a single API call. Open an instance in your account, find the Calls card, and pick what should happen when someone rings:
- Ignore — the
callwebhook fires and nothing else happens. This is the old behaviour, and it stays the default. - Reject the call — the call is turned down, silently.
- Reject and send a message — turned down, followed by the text you write.
- Answer and play media — the instance picks up and plays your audio, and your video on video calls, then hangs up after the number of seconds you set.
The setting sticks with the instance. It applies whether or not you subscribe to the call webhook, so a policy is not something you can accidentally switch off somewhere else.
The same thing over the API, if you would rather configure it from your own code:
curl -X PUT "https://waapi.app/api/v1/instances/{id}" \
-H "Authorization: Bearer $WAAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"callPolicy": {
"mode": "reject_with_message",
"message": "We do not take calls on this number. Send us a message and we will reply."
}
}'
mode is one of ignore, reject, reject_with_message or answer_and_play. reject_with_message needs message; answer_and_play needs audio_url or video_url and takes an optional hangup_after_seconds between 5 and 300.
Driving a call from your own code
When a fixed policy is too blunt — you want to decide per caller, per time of day, per customer record — handle the call webhook and act on it. The call is ringing while you decide, so answer quickly.
# accept the ringing call and play a greeting into it
curl -X POST "https://waapi.app/api/v1/instances/{id}/client/action/accept-call" \
-H "Authorization: Bearer $WAAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"audioUrl": "https://your-domain.com/greeting.mp3",
"hangupAfterSeconds": 30
}'
Leave callId out and the action applies to the instance's only active call, which is what you want in the common case. Pass it when you are tracking several.
Placing a call works the same way:
curl -X POST "https://waapi.app/api/v1/instances/{id}/client/action/start-call" \
-H "Authorization: Bearer $WAAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"chatId": "[email protected]",
"waitForAnswer": true,
"audioUrl": "https://your-domain.com/reminder.mp3",
"hangupAfterSeconds": 20
}'
With waitForAnswer (the default) the response tells you whether the callee actually picked up. If nobody does within answerTimeout, the instance hangs up by itself and you get that back as the result rather than a call left ringing.
The rest of the set: reject-call and end-call to hang up, and play-call-audio, play-call-video and show-call-image to push media into a call that is already connected.
What lands on your webhook
Two events, and the second one is the interesting one.
call fires when a call starts ringing — incoming and outgoing, so a call your own code placed comes back through the same channel.
call_ended fires when it is over, and carries what actually happened: an outcome of Completed, Missed, Rejected, Canceled, AcceptedElsewhere, Failed or Unknown, the peer, the duration in seconds, whether it was a video call, and the message ID of the call-log entry in the chat. That is the event to write into your CRM — call alone tells you the phone rang, not how it went.
Limits, and what "beta" means here
Concrete numbers first:
| Outgoing calls | 10 per minute, 100 per hour, per instance |
| Media files | 25 MB each |
| Audio formats | wav, mp3, ogg |
| Video format | mp4, H.264 |
| Image formats | jpeg, png |
| Hang-up timer | 5 to 300 seconds |
The rate limit is spent when the request is accepted, before the media is fetched — so a bad audioUrl still costs one slot. Check your URLs.
Trial instances are the exception to "every instance": as with messages, an outgoing call from a trial instance can only reach the trial phone number set on your account, and trial instances carry their own tighter rate limit. Incoming calls and call policies are not restricted.
Now the honest part. Calls are marked [BETA] in the API reference, in the MCP tools and on the settings card, and that marking is not decoration. The endpoints work and are in production, but they are the newest and least travelled part of WaAPI, running against a moving target: calling is one of the areas WhatsApp changes most. Request and response shapes may still change while the marking is there, and we will announce it when they do. Anything you build on this today, build so that a failed call is survivable.
If something behaves differently than described here, tell us — during a beta that feedback is worth more than it will ever be again. Open a ticket from the Support page in your account.
The full parameter reference for every call action is in the API documentation.