Receive transfers in your app
A Your app destination hands a live call to software you run: your own contact-center client, a CRM softphone, or another voice system. When the AI transfers, Talkif sends your server a signed webhook describing the call. Your server accepts it with an API key and joins the call audio over a websocket. The caller never hangs up and dials again.
Set it up
Add the destination
Settings → People & numbers → Add destination → Your app. Enter a name and your webhook URL (HTTPS, reachable from the internet). Copy the signing secret shown after you save — it is shown once.
Create an API key for accepting
Create an API key with the scope transfer_offers:* (or transfer_offers:read + transfer_offers:write). It can view, accept and decline transfer offers — nothing else. A key created without scopes gets full access, so always set them. Keep your configuration keys (transfers scope) off the server that receives webhooks.
The offer webhook
For each transfer Talkif sends a POST to your URL:
- Reply
2xxwithin 3 seconds. That means “ringing”. Any other status, a timeout or a connection error counts as a decline for this call. Reply first, then accept — don’t hold the webhook open while you decide. - Accept before
acceptUntil. After that the offer can no longer be taken. caller.numberis the caller’s number in E.164, ornullwhen there is none (a browser test call, for example).caseisnullwhen the AI sent no handoff details.- Webhooks are not retried: a call rings for seconds, so a retry would arrive after the caller is gone.
Verify the signature
v1 is the HMAC-SHA256 of "<t>.<raw request body>" with your signing secret, in hex. During a secret rotation the header carries two v1 values — accept the request if either matches. Reject requests whose t is more than 5 minutes old, and use Talkif-Delivery-Id to drop duplicates.
Always verify against the raw bytes you received, before parsing the JSON.
Accept or decline
Use an API key with the transfer_offers:* scope:
- The first to accept gets the call. A later accept gets
409 offer_taken; an accept afteracceptUntilgets409 offer_expired. - Retrying an accept with the same key returns a fresh join for the same claim.
POST /transfers/offers/{offerId}/decline(204) lets the transfer move on without waiting for the ring timeout.GET /transfers/offers/{offerId}returns the offer as it was delivered.
Full request and response shapes: POST /transfers/offers/{offerId}/accept.
Join within 10 seconds of accepting. An accept that never joins counts as unanswered, and the agent carries on with the caller.
Join the call audio
Open a websocket to joinUrl with the token as the token query parameter (add it if joinUrl doesn’t already carry it). Connect from your server — browser pages can’t open this socket.
The call ends when either side hangs up: you close the socket (or send hangup), or the caller leaves and Talkif closes it.
When an offer is withdrawn
If an offer ends before your app accepted or declined it, Talkif sends a best-effort transfer.offer.withdrawn webhook, signed the same way:
reason is answered_elsewhere (someone else in the group took it), cancelled (the caller hung up or the transfer was stopped) or expired. Use it to stop ringing in your own UI.
Rotate the secret
Rotate secret on the destination (or POST /transfers/destinations/{id}/rotate-secret) returns a new secret once. The old one keeps working for 24 hours (previousSecretExpiresAt): deliveries are signed with both, so you can deploy the new secret without dropping calls.
Limits
Delivery errors are reported as No response within 3 s, Could not connect, Blocked address, TLS error, HTTP <code> or Delivery is unavailable. Webhook URLs must be HTTPS on a public host; private, internal and loopback addresses are refused.