← All posts
Tutorial

Add WhatsApp Voice Calls to Your Support App with UnifyPort

To add WhatsApp voice calling to a support app with UnifyPort, your backend prepares a call with media_type: "voice", then gives the assigned agent a private browser link. The agent opens it and clicks Start call to dial. Your backend associates the returned call_id with the support task and uses call events to follow the outcome.

This tutorial covers human-initiated, one-to-one outbound calls in private beta. UnifyPort uses an unofficial interface; these are not Meta WhatsApp Business Calling API instructions. For the product overview and application process, see the calling beta announcement.

1. Prepare the account and select a conversation

Ask technical support to enable calling for your workspace. Creating an API key does not enable it: is_beta is a read-only eligibility flag, not a setting you can turn on through workspace metadata.

Before showing a call action, check that the WhatsApp messaging account is authorized and its runtime_status is running. Select a private conversation from that same account and preserve its returned conversation_id. Do not substitute a phone number or construct a provider identifier yourself.

In your application, bind the selected workspace, account, conversation and support task to the action. Check the operator’s access on the backend. If a new message arrives or the agent switches conversations, it must not silently change the target of an already prepared call.

The integration below keeps the API key on your server. The browser receives only the private call link it needs.

2. Prepare a voice call on the backend

Replace the placeholders with the workspace API key, connected account and selected conversation. Run this request on the server:

curl -X POST 'https://api.unifyport.ai/v1/calls' \
  -H 'X-Api-Key: <YOUR_API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{
    "account_id": "acc_example",
    "to": { "id": "<conversation_id>", "type": "user" },
    "media_type": "voice"
  }'

The create-call reference documents 201 Created with a response shaped like this. Values below are illustrative, not a live call receipt:

{
  "request_id": "req_example",
  "data": {
    "call_id": "call_example",
    "direction": "outbound",
    "media_type": "voice",
    "state": "preparing",
    "join_url": "https://calls.example.com/call/call_example#ticket=example-only",
    "expires_at": "2026-10-27T08:02:00Z"
  }
}

Save data.call_id with your selected account, conversation and task. Use request_id for troubleshooting. Treat data.join_url as a secret: pass it unchanged to the assigned agent, and exclude the complete URL from logs, analytics and public messages.

201 and preparing mean the link is ready. They do not mean the customer’s phone is ringing or the customer has answered.

3. Let the agent start the call in the browser

Show the selected customer and a “Ready to start” indicator in your app. Open join_url only for the intended operator. In the call page, the agent permits microphone access and clicks Start call before expires_at.

The preparation window defaults to two minutes from creation, but the server can change it. Read expires_at instead of calculating a hard-coded deadline. This window does not limit the duration of a connected call. Microphone access is required; camera access is optional.

Disable duplicate submission while your app is waiting for the create response. If the response is lost, show an unresolved outcome and query the current call before considering another create request. A disabled button alone does not coordinate multiple agents, so handle account-level conflicts on the backend too.

4. Associate webhooks with the support task

For a webhook endpoint using an explicit event list, include both names below alongside any existing subscriptions you still need. This is a field fragment, not a complete endpoint-creation request:

{
  "subscribed_events": ["call.answered", "call.ended"]
}

In beta workspaces, *, an empty list or an omitted list also includes eligible call events. Wildcard subscriptions do not grant call-event access to non-beta workspaces.

ObservationApplication action
Create response: data.call_idAssociate the call with the selected support task
call.answered: data.call.idRecord that this call connected
call.ended: data.call.id, data.reason, data.duration_secondsRecord the end reason and connected duration

Configure a signing_secret on the endpoint before processing signed events, and keep that secret on the server.

Verify webhook signatures using the raw request body and deduplicate by the event envelope’s id, following the delivery contract. Match the event’s account_id and data.call.id to your stored association. Handle delayed or duplicate observations without reopening an ended call in the UI.

Keep “answered” evidence separately: duration_seconds is rounded down, so 0 alone does not prove the call was unanswered. A synchronous create failure may have no end event; record HTTP failures separately. See the answered event and ended event references for payloads. An ended call does not automatically mean the support task is resolved.

5. Query or end the managed call

Use the active-call endpoint when recovering state or a lost call identifier:

curl --get 'https://api.unifyport.ai/v1/calls/active' \
  -H 'X-Api-Key: <YOUR_API_KEY>' \
  --data-urlencode 'account_id=acc_example'

data: {} means there is no current call managed by UnifyPort for that account. It is not a call-history response, proof of the previous attempt’s outcome, or a way to detect calls in the native WhatsApp app. The response does not reissue the one-time join_url.

When the operator chooses to end a known call, use the stored call_id and its account:

curl -X POST 'https://api.unifyport.ai/v1/calls/call_example/hangup' \
  -H 'X-Api-Key: <YOUR_API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{"account_id":"acc_example"}'

The hangup reference returns a terminal state of ended or failed; repeating a hangup can return the existing terminal state. Do not repeat it merely because the returned state is failed, and do not end another agent’s call just to clear a create conflict.

Common integration problems

SituationNext step
403 beta_feature_not_enabledConfirm workspace eligibility with support; metadata changes cannot enable it
Preparation link expiredStop reusing it; check the current call and let the agent explicitly decide whether to prepare another
Browser cannot use the microphoneCheck the call page’s microphone permission before expecting dialing to work
Create conflicts with an existing callQuery the managed call and identify its owner and purpose in your app
Create or hangup request times outKeep the result unresolved; reconcile current state and received events before retrying

Validate one complete support flow

In an approved test conversation, check successful connection and hangup, an expired preparation link, denied microphone permission, duplicate clicks, an account conflict and a lost HTTP response. Confirm that event redelivery does not create duplicate task records and that full call links never enter logs. These are acceptance checks for your integration, not results claimed by this article.

Can this automatically dial a list of phone numbers?

No. This flow requires an account-scoped conversation_id and a human clicking Start call in the browser.

Can the same integration answer incoming calls?

Not in the current beta. call.incoming is reserved; its presence in the event catalogue does not enable inbound calling. Group calls, recording, transcription, SIP/PBX integration and AI voice agents are also outside this beta’s scope.

Where should a team start?

If eligibility is not enabled, contact technical support to apply with your workspace ID and intended support scenario. Do not send an API key or private call link. Once enabled, use the create-call reference to test one selected conversation before rolling the action into your support workflow.

UnifyPort API

Turn messaging integration into a stable product pipeline.

Start by sending through one API, then bring every inbound message back into your business system with standard events.