The outbound call API in one POST and two GETs
One POST to the outbound call API places the call, one WebSocket frame says when it ended, and two GETs return the call transcript and the cost breakdown.

Updated 14 September 2026 with figures: the real-time event stream replaced polling as the way to learn a call ended, and the public API reference now lives at docs.talkif.ai.
Placing a call from your own code is one POST to the outbound call API. Knowing when it ended is one WebSocket frame. Getting the conversation and the bill back is two GETs. Everything in between — the dialling, the agent talking, the hang-up, the pricing — happens on Talkif, and what comes back to you is a record: a transcript with a speaker and a timestamp on every turn, and a cost broken down by what was actually consumed. This post walks that path once, end to end, with the real request and response shapes, so you can paste it and run it.
The SDKs for TypeScript and Python are being cut; until they are on npm and PyPI, everything below is plain HTTPS and works from any language.
A key that can do this, and nothing else
An API key belongs to one account and acts for it; you never send an account id. It is created in the dashboard under Developer → Credentials, shown once, and sent as a bearer token. What matters for this guide is the scope: the key needs to place calls and read their results, so give it calls:* — that covers placing, history, transcripts, recordings and the event stream — and billing:read for the cost breakdown. A key that also holds flows or contacts can do things this integration will never need, and a leaked key does everything its scopes allow.
Two restrictions are worth setting on a key that will live on a server. An IP allowlist in CIDR notation rejects requests from anywhere else with a 403; an expiry date turns the key off without anyone remembering to revoke it. Both are optional and both are per key. The full reference is at docs.talkif.ai/integrate/authentication.

export TALKIF_API_KEY=tif_live_…Place the call
A call needs four things: the number to dial, the number to dial from, the flow to run, and the provider that owns the from-number. The provider id is on the number's record, so the first request is usually a lookup you make once and cache:
curl "https://api.talkif.ai/api/v1/phone/numbers" \
-H "Authorization: Bearer $TALKIF_API_KEY"Then the call. Keep the callId from the response against your own record — an order id, a ticket — because that id is how every later request refers to the call:
curl -X POST "https://api.talkif.ai/api/v1/calls" \
-H "Authorization: Bearer $TALKIF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"toNumber": "+15551234567",
"fromNumber": "+15559876543",
"flowId": "550e8400-e29b-41d4-a716-446655440000",
"providerId": "550e8400-e29b-41d4-a716-446655440000"
}'There are two success shapes, and your code has to handle both. 201 with type: "initiated" means the call is being placed now and carries the callId. 202 with type: "queued" means the account is at its concurrent-call limit; the call will be placed when a slot frees, and the response carries position and estimatedWaitSeconds instead of a call id — the call record, and its id, arrive on the event stream when it is actually created. Before either, the number is checked against the account's do-not-call list and the balance against the minimum to start a call; each refusal is a distinct 4xx with its own error code, listed on the make-a-call page.
One thing POST /calls does not accept is a client idempotency key. Brandur Leach's Stripe essay on the subject describes the situation that creates the need: "In many others though, the success of the operation is ambiguous from the perspective of the client, and it doesn't know whether retrying the operation is safe. A connection terminating midway through message exchange is an example of this case" (Stripe, 2017). For a phone call the wrong answer is worse than a duplicate charge — it is a second phone ringing at the same person. So on an ambiguous failure, do not retry blind: GET /api/v1/calls?phoneNumber=%2B15551234567&startDate=… lists recent calls to that number, and a call created in the last few seconds from your from-number is the one whose response you lost.
Know the moment it ends
Polling GET /calls/{callId} every second works and tells you about a second late. The event stream is the same feed the dashboard runs on, and it is exposed over a WebSocket authenticated with the same key:
import WebSocket from "ws";
const ws = new WebSocket("wss://api.talkif.ai/api/v1/ws/events", {
headers: { Authorization: `Bearer ${process.env.TALKIF_API_KEY}` },
});
ws.on("open", () => ws.send(JSON.stringify({ type: "subscribe" })));
ws.on("message", (raw) => {
const frame = JSON.parse(raw.toString());
if (frame.type === "call.ended") {
const { callId, status, endReason, duration, cost } = frame.data;
// fetch the transcript and the breakdown for callId
}
});A subscribe frame with no call_id is the account-wide stream: call.created when a record exists, call.status on every change through initiated → ringing → inprogress, and call.ended once, with the terminal status (completed, failed, busy, noanswer, canceled), an endReason, the duration in seconds and the cost as a USD string. Subscribing to a specific call_id adds the conversation itself — each finalised turn as a transcript frame while the caller is still on the line — and "replay": true on that subscribe replays the call's history first, so a consumer that restarts mid-call still gets the whole thing.
The statuses a call moves through are the ones the record and the frames both report:

The shape will be familiar if you have used a carrier API directly. Twilio's call resource, for instance, reports the same terminal set and delivers it the older way — "After completing an outbound call, Twilio will make an asynchronous HTTP request to the StatusCallback URL you specified in your request (if any)" (Twilio, Call resource). A callback needs a public URL on your side; the frame needs only an outbound socket, which is why the stream is the default here and a callback is not.
Open one connection per service instance, not per call; re-subscribe after any close. The connection limits and the ping cadence are on the real-time events page.
Read the call transcript
curl "https://api.talkif.ai/api/v1/calls/$CALL_ID/transcript" \
-H "Authorization: Bearer $TALKIF_API_KEY"The response has both forms. transcript is the conversation as one string, for a CRM note. messages is the structured form: one entry per speech segment with speaker (ai or human), content, timestampMs from the start of the call, durationMs, and for the agent's turns the llmModel that produced them and a metrics object with the turn's latency and token usage. If you want to know why an agent said something, messages is the one to keep.
The call record itself, GET /api/v1/calls/{callId}, carries the rest: status, endReason, failureCode and failureReason when it did not complete, duration, firstSpeechMs — how long the caller waited for the first word — interruptedTurns, the function-call count and total time, and whether a recording exists. A recording is fetched with GET /calls/{callId}/recording.
Read what it cost, line by line
cost on the call is a single number. The breakdown is a separate request, and it is the same record the billing engine wrote when the call was priced:
curl "https://api.talkif.ai/api/v1/billing/costs/calls/$CALL_ID" \
-H "Authorization: Bearer $TALKIF_API_KEY"lineItems is one entry per component — the language model's input and output tokens, speech synthesis characters, transcription seconds, carrier minutes, the platform fee — each with usageQuantity and its usageUnit, rateMicrocents per rateUnit, and amountMicrocents. totalMicrocents is the integer sum in millionths of a dollar, which is the unit every amount is computed in; totalUsd is the same number as a decimal for display. The rate on each line is the rate that applied when the call was priced, copied into the record, so a later price change does not rewrite history. The categories and what falls into each are on the cost breakdown page.
Developers
Turn detection that doesn't talk over you
How our voice agent's turn detection decides a caller has finished, when an interruption is real and when to stay quiet, and the incidents behind it.
Integrations
Meta lead ads, called on arrival and reported back
Connect Meta lead ads to Talkif: each lead is called within your calling hours, and received, contacted, qualified and converted go back to Meta.
Developers
Webhook SSRF, closed by a sender that holds nothing
Webhook SSRF closed twice: the process with your secrets never sends, and the process that sends can reach only the public internet.
Developers
Voice agent function calling with bound parameters
Voice agent function calling on Talkif: describe the endpoint once, then bind each parameter to the model, the call or a fixed value no caller can spoof.
Questions about this piece? Write to us.



