For developers
Connect Telofia to the systems you already use
Read calls, transcripts, appointments and contacts over a REST API, get signed webhooks the moment something happens, let the assistant check live data in your system during a call, and put a voice assistant on your website with one line of code.
The REST API and webhooks are included in every plan, starting with Line; higher plans have higher limits. During the free trial you get test access: 1 API key, 20 requests per minute.
https://telofia.com/api/v1curl https://telofia.com/api/v1/me \
-H "Authorization: Bearer tf_live_…"{
"object": "account",
"id": "1f2e3d4c-5b6a-4789-8a7b-6c5d4e3f2a1b",
"name": "Smile Dental",
"timezone": "Europe/Warsaw",
"locale": "en",
"country": "PL",
"currency": "PLN",
"status": "active",
"plan": {
"id": "team",
"billing_cycle": "month",
"trial_ends_at": null,
"period_start": "2026-10-01T00:00:00Z",
"period_end": "2026-11-01T00:00:00Z",
"recording_history_days": 365
},
"minutes": {
"included": 3000,
"extra": 100,
"total": 3100,
"used": 412.5,
"remaining": 2687.5
},
"api_key_id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d",
"scopes": [
"read:calls",
"read:appointments",
"read:contacts",
"read:tasks"
],
"limits": {
"plan": "team",
"requests_per_minute": 180,
"requests_per_day": 50000,
"max_api_keys": 10,
"max_webhook_endpoints": 10
}
}What you can connect
Seven ways to plug Telofia into your business
Pick what fits: no-code automations, a few lines of code, or a full two-way integration.
- Every plan
REST API
Read calls with transcripts and recordings, appointments, contacts and tasks. Create and update contacts, cancel or move appointments and close tasks from your own system.
- Every plan
Webhooks
Signed JSON events in real time: call started or completed, appointment booked, canceled or moved, new contact, callback task.
- Every plan
Live data via MCP
Connect your MCP server and the assistant checks stock, availability or order status during the call.
- From Office
Website voice widget
One script tag adds a “Talk to us” button. Visitors speak with your assistant in the browser.
- Every plan
Phone onboarding
Send new sign-ups from your system and the assistant calls them to help them get started.
- Every plan
Calendar & CRM
Google Calendar for bookings, HubSpot and Pipedrive for contacts, calls, meetings and tasks.
- Every plan
Zapier, Make, n8n
No-code flows built on webhooks and the API: Google Sheets, Slack, email, any CRM.
Quick start
Your first request in two minutes
- 1
Create an API key
In the dashboard go to Developers → API keys, name the key and choose its scopes. The key (tf_live_…) is shown only once, so store it in your secrets manager.
- 2
Test the connection
Call GET /me. It works with any valid key and returns your account, the key's scopes and your plan's limits.
- 3
Read data or subscribe to events
List calls, appointments and contacts, or add a webhook endpoint under Developers → Webhooks to get events as they happen.
curl https://telofia.com/api/v1/me \
-H "Authorization: Bearer tf_live_…"const API = "https://telofia.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.TELOFIA_API_KEY}` };
// All calls booked since 1 October, page by page
let cursor = null;
while (true) {
const url = new URL(`${API}/calls`);
url.searchParams.set("outcome", "booked");
url.searchParams.set("since", "2026-10-01T00:00:00Z");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers });
if (res.status === 429) {
// rate limited: wait for the number of seconds in Retry-After, then retry the same page
await new Promise((r) => setTimeout(r, Number(res.headers.get("Retry-After") ?? 1) * 1000));
continue;
}
if (!res.ok) throw new Error((await res.json()).error.message);
const page = await res.json();
for (const call of page.data) console.log(call.started_at, call.from, call.summary);
if (!page.has_more) break;
cursor = page.next_cursor;
}REST API v1
Calls, appointments and contacts over HTTPS
JSON in, JSON out. Every request is authenticated with an API key and limited by your plan. Requests are logged in the dashboard (method, path, status and time — never request or response bodies).
Authentication
Create a key in API keys and send it as a Bearer token. Keys start with tf_live_ and belong to one account.
Authorization: Bearer tf_live_…Pagination
List endpoints return up to limit items (1–100, default 25) in a stable order. When has_more is true, pass next_cursor unchanged as cursor (with the same filters) to get the next page. An invalid cursor returns 400 invalid_request.
Errors
Errors use standard HTTP status codes and a JSON body with a type and a human-readable message.
{ "error": { "type": "…", "message": "…" } }Endpoints
- GET
/meScope: any keyGet the current account
Returns the account the API key belongs to. Handy as a “test connection” call.
- GET
/assistantsScope: any keyList assistants
Your assistants with their language, status and assigned phone numbers. Use an id as assistant_id when filtering calls.
- GET
/assistants/{id}Scope: any keyRetrieve an assistant
One assistant with language, status and phone numbers. With the read:assistants (or write:assistants) scope it also returns the prompt: instructions, greeting, company_profile and updated_at.
- PATCH
/assistants/{id}Scope: write:assistantsUpdate the assistant's prompt
Changes the instructions, greeting or company profile. Omitted fields stay unchanged. Same limits as the dashboard: instructions up to 20,000 characters, company profile up to 10,000, greeting 5–600 characters and it must say the caller is talking to an AI (otherwise 400 with code ai_disclosure). Every change is saved in the assistant's version history (source API) and applies from the next call. The platform's honesty rules (AI disclosure, recording notice) always apply on top of your instructions.
- GET
/phone-numbersScope: any keyList phone numbers
Your account's phone numbers with country, type, status and the assistant that answers them.
- GET
/callsScope: read:callsList calls
Calls newest first, without transcripts. Demo calls are never included.
- GET
/calls/{id}Scope: read:callsRetrieve a call
One call with the full transcript and the appointments booked during it.
- GET
/calls/{id}/recordingScope: read:callsGet a call recording
Signed links, valid for 1 hour, to play and download the call's MP3 recording (stereo: caller on the left, assistant on the right). A recording older than your plan's recording history returns 403 plan_required with recording_history_days; a call without a recording returns 404.
- GET
/appointmentsScope: read:appointmentsList appointments
Upcoming appointments in start order (from now by default).
- GET
/appointments/{id}Scope: read:appointmentsRetrieve an appointment
One appointment of your account.
- POST
/appointments/{id}/cancelScope: write:appointmentsCancel an appointment
Cancels an upcoming appointment, removes it from the connected Google calendar and sends the appointment.canceled webhook. calendar_sync tells you whether the calendar was updated.
- POST
/appointments/{id}/rescheduleScope: write:appointmentsReschedule an appointment
Moves an upcoming appointment to a new time. Fails with 409 if the time overlaps another appointment or a busy time in the connected calendar. Without ends_at the length stays the same. Sends the appointment.rescheduled webhook.
- GET
/contactsScope: read:contactsList contacts
Your callers, newest first. The assistant remembers them across calls.
- GET
/contacts/{id}Scope: read:contactsRetrieve a contact
One contact with name, email, tags, notes and where the name and email came from (call, system or manual).
- POST
/contactsScope: write:contactsCreate a contact
Adds a contact, for example from your CRM, so the assistant knows the caller's name. Name and email saved through the API are marked manual and the assistant never overwrites them. Returns 409 with contact_id if the phone number already exists. Sends the contact.created webhook.
- PATCH
/contacts/{id}Scope: write:contactsUpdate a contact
Changes the name, email, notes or blocked flag. Omitted fields stay unchanged and null clears a field. A changed name or email is marked manual, so the assistant won't overwrite it. The phone number can't be changed.
- GET
/tasksScope: read:tasksList tasks
Callback requests, messages and other tasks from calls, newest first.
- GET
/tasks/{id}Scope: read:tasksRetrieve a task
One task of your account.
- PATCH
/tasks/{id}Scope: write:tasksUpdate a task
Sets the task status to open, done or dismissed, for example after your team has called the customer back.
- POST
/calls/outboundScope: write:callsCreate an outbound call
Your assistant calls one person from your business number with the goal you give, like a step of an onboarding sequence. The call goes out now or at call_at, always within calling hours (Mon–Sat, 9:00–20:00 in your account's time zone; outside them it moves to the next allowed time and call_at shows when). If nobody answers, it tries up to 3 times, 2 hours apart. Returns 202 with the call in status scheduled. Errors: 400 consent_missing, invalid_phone, number_not_callable, international or invalid_call_at; 409 opted_out (the person asked not to be called), already_scheduled (with existing_id), assistant_not_live, over_quota or no_outbound_number; 429 daily_outbound_limit with limit (200 calls per 24 hours by default). The outcome arrives in the call.completed and outbound_call.finished webhooks with your external_id.
- GET
/calls/outbound/{id}Scope: write:callsRetrieve an outbound call
Status of a scheduled outbound call: scheduled, dialing, done (with call_id of the conversation), failed (last_error, e.g. no_answer after all attempts) or canceled. Also works with the callback_id from webhooks for onboarding and callback calls.
- DELETE
/calls/outbound/{id}Scope: write:callsCancel an outbound call
Cancels an outbound call created through the API that is still scheduled. Repeating it is safe (already canceled returns 200). Returns 409 not_cancelable when the call is dialing or finished, or for onboarding calls (remove the person with POST /api/outreach/unenroll instead).
- GET
/messagesScope: read:messagesList SMS messages
SMS your account sent and customer replies, newest first. phone is the customer's number (recipient or sender of a reply). generated_by: template, ai or manual; charged_from: plan or credits.
- POST
/messagesScope: write:messagesSend an SMS
Sends an SMS to one of your contacts or callers: your own text, or one of your account's templates with vars (date, time, name, service, link, amount). Counts toward your plan's SMS allowance and extra SMS. Errors: 400 invalid_request (consent_missing, invalid_phone, too_long, missing_vars); 409 conflict (opted_out — the customer replied STOP, over_quota, recipient_not_allowed — create the contact first, blocked_destination); 429 rate_limited.
Scopes
Every key has the scopes chosen when it was created. Scopes can't be changed later, so create a new key when you need more. Endpoints answer 403 insufficient_scope when the key lacks the required scope. GET /me, /assistants and /phone-numbers work with any valid key.
read:callsRead calls, summaries and transcriptsread:appointmentsRead appointmentswrite:appointmentsCancel and reschedule appointmentsread:contactsRead contactswrite:contactsCreate and update contactsread:tasksRead tasks and callback requestswrite:tasksUpdate task statuswrite:callsStart outbound calls, check and cancel themread:messagesRead SMS messages and customer replieswrite:messagesSend SMS to your contactsread:assistantsRead the assistant's prompt (instructions, greeting, company profile)write:assistantsChange the assistant's prompt
Errors
400invalid_request — a parameter is missing or invalid401unauthorized — missing, invalid or revoked API key403insufficient_scope — the key lacks the endpoint's scope; plan_required — the account has no active plan, or the recording is older than the plan's recording history404not_found — the object doesn't exist in your account409conflict — the time is already taken, the appointment can't be changed, a contact with this phone number already exists, or an outbound call can't be made (code: opted_out, already_scheduled, assistant_not_live, over_quota, no_outbound_number, not_cancelable)429rate_limited — too many requests; retry after the Retry-After header500server_error — something went wrong on our side503service_unavailable — a temporary problem; retry after the Retry-After header
Full API reference with parameters and example responses is in the dashboard under Developers → API reference.
Webhooks
Events pushed to your server in real time
Add an https endpoint in the dashboard, choose the events, and we POST a signed JSON envelope as soon as something happens. “Send test” delivers a sample of your first event, handy for mapping fields in Zapier or Make.
Events
call.completedCall completedSummary, outcome, sentiment, transcript, contact and bookings; for outbound calls also the sequence or API request (callback_id, external_id, outreach).
call.startedCall startedA call was answered (inbound, outbound or web).
appointment.bookedAppointment bookedThe assistant booked an appointment.
appointment.canceledAppointment canceledAn appointment was canceled by phone or through the API.
appointment.rescheduledAppointment rescheduledAn appointment was moved to a new time (includes the previous time).
task.createdCallback / task createdA caller asked for a callback or left a task.
contact.createdNew contactA first-time caller was saved as a contact.
outbound_call.finishedOutbound call finishedA scheduled outbound call (API, onboarding sequence or callback) is done or failed — also when nobody answered after all attempts.
Headers on every delivery
Telofia-SignatureSignature: t=<unix time>,v1=<HMAC-SHA256 hex>Telofia-EventEvent type, e.g. appointment.bookedTelofia-DeliveryUnique delivery IDUser-AgentTelofia-Webhooks/1.0 · Identifies our webhook sender
Delivery and retries
- Answer with any 2xx status within 10 seconds. Redirects are not followed.
- Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h (7 attempts, about 21 hours).
- The same event can arrive more than once. Use the event id to skip duplicates.
- Only public https addresses are accepted. After 50 failures in a row an endpoint is paused and the dashboard tells you why.
- The dashboard keeps a delivery log with status codes and responses, and lets you retry a delivery by hand.
{
"id": "evt_3f9a1c7e5b2d4f6a8c0e1b3d5f7a9c1e",
"type": "call.completed",
"created_at": "2026-10-01T09:17:45Z",
"account_id": "1f2e3d4c-5b6a-4789-8a7b-6c5d4e3f2a1b",
"data": {
"id": "5b1f0c6e-2a8d-4f3e-9d51-7c0a4e2b9f10",
"object": "call",
"assistant_id": "0e6f4c8a-3b2d-4a1e-8f7c-5d9b2a1c3e4f",
"phone_number_id": "4d3c2b1a-0f9e-4d8c-b7a6-958473625140",
"contact_id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"direction": "inbound",
"status": "completed",
"from": "+48601234567",
"to": "+48221234567",
"started_at": "2026-10-01T09:14:03Z",
"ended_at": "2026-10-01T09:17:41Z",
"duration_sec": 218,
"summary": "Anna Kowalska booked a first consultation for Friday 10:00.",
"outcome": "booked",
"sentiment": "positive",
"tags": [
"new-customer"
],
"has_recording": true,
"end_reason": "caller_hangup",
"callback_id": null,
"external_id": null,
"outreach": null,
"caller_spoke": true,
"caller_words": 6,
"transcript": [
{
"role": "caller",
"text": "I'd like to book a consultation this week.",
"at": 4.1
}
],
"contact": {
"id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"object": "contact",
"phone": "+48601234567",
"name": "Anna Kowalska",
"name_source": "call",
"email": "anna.kowalska@example.com",
"email_source": "call",
"notes": null,
"tags": [
"customer"
],
"blocked": false,
"calls_count": 3,
"last_call_at": "2026-10-01T09:14:03Z",
"created_at": "2026-08-12T15:02:11Z"
},
"appointments": [
{
"id": "c3d2e1f0-a9b8-4c7d-8e6f-5a4b3c2d1e0f",
"object": "appointment",
"call_id": "5b1f0c6e-2a8d-4f3e-9d51-7c0a4e2b9f10",
"contact_id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"customer_name": "Anna Kowalska",
"customer_phone": "+48601234567",
"customer_email": "anna.kowalska@example.com",
"service": "First consultation",
"starts_at": "2026-10-03T08:00:00Z",
"ends_at": "2026-10-03T08:30:00Z",
"status": "booked",
"external_id": "google:7h3k9s2l1m0n",
"created_at": "2026-10-01T09:16:20Z"
}
]
}
}Verifying signatures. Every delivery has a Telofia-Signature header in the form t=timestamp,v1=signature. Compute HMAC-SHA256 of “timestamp.raw_body” with your signing secret and compare it to v1. Reject timestamps older than 5 minutes.
import crypto from "node:crypto";
// Express: app.post("/webhook", express.raw({ type: "application/json" }), handler)
export function verify(rawBody, signatureHeader, secret) {
// Telofia-Signature: t=1727774265,v1=5f2c…
const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const given = Buffer.from(parts.v1 ?? "");
// timingSafeEqual needs buffers of the same length
return fresh && given.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), given);
}import hmac, hashlib, time
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < 300
return fresh and hmac.compare_digest(expected, parts.get("v1", ""))import express from "express";
import { verify } from "./verify.js";
const app = express();
const seen = new Set(); // use your database in production
app.post("/telofia/webhook", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verify(raw, req.get("Telofia-Signature") ?? "", process.env.TELOFIA_WEBHOOK_SECRET)) return res.sendStatus(401);
const event = JSON.parse(raw);
if (seen.has(event.id)) return res.sendStatus(200); // retried delivery, already handled
seen.add(event.id);
if (event.type === "appointment.booked") {
const a = event.data;
console.log("New booking:", a.customer_name, a.service, a.starts_at);
}
res.sendStatus(200); // answer with 2xx within 10 seconds
});
app.listen(3000);Stable values
These values in calls (API and call.* webhooks) and in outbound calls (outbound_call.finished, /calls/outbound) are a stable contract: we may add new values, but we won't change or remove these. Unanswered, busy or rejected outbound attempts never create a call, so there is no call.completed for them: you get outbound_call.finished with last_error no_answer after the last attempt. A call answered by voicemail counts as answered (usually a short call with outcome other).
Call status
in_progress- The call is going on.
completed- The person talked to the assistant.
missed- Nobody spoke, or the assistant couldn't take the call (see end_reason).
failed- The call couldn't be handled because of an error.
Call outcome
booked- An appointment was booked.
transferred- The call was transferred to a person.
message- A message, callback request, order or follow-up was taken.
info- The person got information; nothing else was needed.
spam- Spam, telemarketing or a blocked number.
other- Anything else, e.g. a very short call or voicemail.
Call end reason (end_reason)
caller_hangup- The other person hung up.
caller_hangup_greeting- Outbound call: the person hung up during or right after the greeting without saying a word (call of up to 45 s). caller_spoke is then false.
agent_hangup- The assistant ended the call after saying goodbye.
transferred- The call was transferred to a person.
silence- Ended after a long silence.
max_duration- The maximum call length was reached.
spam- Ended as spam.
blocked- The number is blocked in contacts.
busy- All of the account's lines were busy.
over_quota- No call minutes left.
trial_expired- The free trial has ended.
inactive- No active plan.
assistant_not_live- The assistant is paused.
no_assistant- No assistant answers this number.
ai_budget- Handled without AI because of a temporary limit; the team was asked to call back.
error- A technical error ended the call.
Outbound call status
scheduled- Waiting for its time (or for the next attempt).
dialing- Calling now.
done- Answered; call_id is the conversation.
failed- Not answered after all attempts, or couldn't be made (see last_error).
canceled- Canceled (API, the person removed from the sequence, or the sequence turned off).
Outbound call last_error
no_answer- Nobody answered (also busy or rejected).
failed- The assistant wasn't available at call time (plan, minutes or pause).
no_result- No result from the call within 15 minutes.
expired- The call was more than 2 hours late, so it wasn't made.
outside_window- Moved to the next calling hours (status stays scheduled).
dial_error- The phone network refused the call (sent as dial_<reason>).
over_quota- No call minutes left.
inactive- No active plan.
assistant_not_live- The assistant is paused.
no_outbound_number- No business number to call from.
canceled_by_api- Canceled with DELETE /calls/outbound/{id}.
unenrolled- The person was removed from the sequence (POST /api/outreach/unenroll).
opt_out- The person asked not to be called.
replaced- Replaced by a newer callback to the same number.
Live data via MCP
Let the assistant look things up in your system during a call
Connect a remote MCP server (Model Context Protocol), such as your shop catalogue, stock, booking or order system. You choose exactly which read-only tools and resources the assistant may use.
Learn in advance
For content that rarely changes, such as price lists, product descriptions or policies. Selected resources and read-only tools are imported into the assistant's knowledge every 1 to 168 hours or on demand. No delay during calls.
Check live in calls
For things that change: stock, availability, order status. The assistant calls the tool during the conversation with a short “one moment, let me check”. If your server doesn't answer within about 2.5 seconds, it says it can't confirm right now and offers a follow-up from your team.
What you need
- An MCP server reachable over https (Streamable HTTP, e.g. https://mcp.example.com/mcp; older HTTP+SSE servers are detected automatically).
- Authentication: none, a Bearer token or a custom header such as X-API-Key. Secrets are encrypted (AES-256-GCM) and never sent to the browser.
- Read-only tools. Tools marked as destructive are blocked; for tools not marked read-only you confirm that they only read data.
- Up to 5 sources and 50 live tools per assistant. Live lookups should answer within about 2 seconds.
- Optional for the website widget: on an “At the start of the call” tool, pick the argument that receives the identity token of the signed-in visitor (see Website voice widget).
Tool arguments are validated against the tool's schema, results are shortened and treated strictly as data (never as instructions), and every lookup is logged without caller data. Private and internal addresses are refused.
Website voice widget
Your assistant on your website, one line of code
Visitors click a button and talk to the same assistant that answers your phone, right in the browser. Web calls use your plan minutes, with no telephony costs.
From Office
- About 14 KB, no dependencies, isolated in Shadow DOM so your site's styles can't break it.
- Speaks the language of your page (<html lang>) or the one you set with data-lang="de".
- Live captions; if the microphone is blocked, visitors can type and the assistant answers by voice.
- Restrict the widget to your own domains and it only starts on those sites.
<script src="https://telofia.com/widget.js" data-key="tfw_…" async></script>// optional: open the widget from your own button
document.querySelector("#talk-to-us").addEventListener("click", () => window.TelofiaWidget?.open());
window.TelofiaWidget?.close(); // close the panel
window.TelofiaWidget?.destroy(); // remove the widget from the pageStrict Content Security Policy? Allow script-src https://telofia.com https://cdn.jsdelivr.net, connect-src https://telofia.com wss://*.livekit.cloud https://*.livekit.cloud and media-src blob:.
Signed-in users (identity token)
When visitors are signed in on your site, the assistant can know who is talking without asking for their e-mail.
- Your server issues a short-lived, signed token for the signed-in user (e.g. HMAC, valid 15 minutes). Never put an e-mail address or user ID in the browser: anyone could type someone else's into the console.
- Call TelofiaWidget.identify({ token }) at any time; a later call replaces the token and identify(null) clears it. Before widget.js has loaded, set window.TelofiaIdentity = { token, expires_at }: it is read when the conversation starts, and an expired token is skipped.
- In the dashboard, choose the identity token argument of your “At the start of the call” MCP tool. Telofia passes the token unchanged only to that argument, never stores or shows it and keeps it out of transcripts, summaries, webhooks and logs; the assistant never sees it.
- 8 to 400 characters: letters, digits and . _ ~ + / = - (e.g. base64url). Refresh it before it expires, for example every 10 minutes. Your MCP server verifies it; an invalid or expired token should give the same result as no token. Phone calls and conversations without a token work as before.
// signed-in user: a short-lived, signed token issued by YOUR server (never an e-mail or user ID)
const { token, expires_at } = await fetch("/api/telofia-identity").then((r) => r.json());
window.TelofiaIdentity = { token, expires_at }; // read when the conversation starts, also before widget.js loads
window.TelofiaWidget?.identify({ token, expires_at }); // replaces the token at any time
// refresh before it expires (e.g. every 10 minutes); on sign-out:
window.TelofiaIdentity = null;
window.TelofiaWidget?.identify(null);Phone onboarding
Your new customers get a welcome call, automatically
When someone signs up in your system, send them to a call sequence. The assistant calls them from your business number, helps them get started and checks in again a few days later. Calls go out within the sequence's call hours: by default Monday to Saturday, 9:00 to 20:00 in your time zone, or the days and hours you set.
POST/api/outreach/enroll
Authenticate with the sequence key (tlo_…) from the dashboard, as a Bearer token or in the X-Telofia-Key header. Each sequence has its own key.
Request body
- Phone number, ideally in E.164 format. National numbers are read with your account's country code.
phonestringrequired - Must be true: the person agreed to be contacted by phone, e.g. in your sign-up form.
consenttruerequired - Name, up to 120 characters.
namestringoptional - Email address.
emailstringoptional - What the assistant should know for this person, up to 1,000 characters (e.g. the plan they chose).
contextstringoptional - The person's ID in your system (string or number). The same ID is enrolled only once.
external_idstring | numberoptional
curl -X POST "https://telofia.com/api/outreach/enroll" \
-H "Authorization: Bearer tlo_…" \
-H "Content-Type: application/json" \
-d '{"phone":"+48601234567","name":"Anna Kowalska","email":"anna@example.com","context":"Signed up today, Start plan","external_id":"user_123","consent":true}'{
"ok": true,
"enrollment_id": "8f7e6d5c-4b3a-4c2d-9e1f-0a9b8c7d6e5f",
"external_id": "user_123",
"status": "active",
"next_call_at": "2026-10-09T10:20:00Z",
"limits": {
"per_day": 500,
"remaining_today": 499
}
}Responses
- 201: enrolled. enrollment_id, external_id, status, next_call_at (time of the first call), intro_sms_at (when the SMS before the first call goes out, or null) and limits { per_day, remaining_today }
- 200: already enrolled (same external_id, or this number is in progress in the sequence), with duplicate: true
- 400: invalid_request, invalid_phone or consent_required
- 401: invalid_key
- 409: sequence_disabled, consent_missing or opted_out (the person asked not to be called)
- 429: limit, the sequence's daily enrollment limit was reached (500 new people per 24 hours; the body has limit). rate_limited: more than 120 requests per minute with this key (Retry-After header)
Call hours: set the days and a time range between 7:00 and 21:00 for each sequence in the dashboard. If nobody answers, the assistant tries up to 3 times, 2 hours apart, within those hours.
SMS before the call: when it's on for the sequence (dashboard), the person gets a short text from the number the assistant will call from, by default 20 minutes before the first call (5–120 min). If the first call would come sooner, it moves so the SMS always goes out first, within the call hours. A failed SMS (the person replied STOP, the SMS allowance is used up) never stops the call. Each SMS counts toward your SMS allowance and appears in the dashboard under SMS.
Greeting: in the dashboard you set the first sentence of the call for each step (or a default for the whole sequence) — a fixed text with the variables {first_name}, {first_name_vocative} (Polish vocative, e.g. Krystianie), {assistant_name}, Telofia (the company name for calls from the assistant settings) and {name}, with a separate variant for sign-ups without a name — or a mode in which the assistant composes the first sentence from name, context, the step's goal and the result of the "At the start of the call" tools. That an AI assistant is calling, and that the call is recorded, is always added to the first sentence if the text does not say it.
Remove a person from the sequence
POST/api/outreach/unenroll
When someone no longer needs the calls (for example after their first sale, or they cancelled), stop their sequence with the same key. Scheduled calls are cancelled at once. A call already in progress isn't interrupted, but no further calls follow it.
- Exactly one of: external_id (as sent when enrolling), enrollment_id (from the enroll response) or phone.
external_id | enrollment_id | phonerequired - Optional reason, up to 200 characters, kept in your account's audit log.
reasonoptional
- 200: stopped (changed: true) or already finished: status stopped, completed or failed (changed: false). Safe to repeat.
- 400: invalid_request (no identifier or more than one) or invalid_phone
- 401: invalid_key
- 404: not_found, no such enrollment in this sequence
- 429: rate_limited
curl -X POST "https://telofia.com/api/outreach/unenroll" \
-H "Authorization: Bearer tlo_…" \
-H "Content-Type: application/json" \
-d '{"external_id":"user_123","reason":"first_sale"}'{
"ok": true,
"enrollment_id": "8f7e6d5c-4b3a-4c2d-9e1f-0a9b8c7d6e5f",
"external_id": "user_123",
"status": "stopped",
"changed": true
}Call results in webhooks
call.started and call.completed for calls from a sequence carry callback_id, external_id and outreach { sequence_id, enrollment_id, external_id, step } (all null for inbound calls). Unanswered attempts don't create a call, so there is no call.completed for them: after the last attempt you get outbound_call.finished with status failed and last_error no_answer. Retries of one step count as one result. call.completed also includes caller_spoke and caller_words (how many words the person said). When someone answered and hung up during or right after the greeting without a word, end_reason is caller_hangup_greeting.
{
"id": "5b1f0c6e-2a8d-4f3e-9d51-7c0a4e2b9f10",
"object": "call",
"assistant_id": "0e6f4c8a-3b2d-4a1e-8f7c-5d9b2a1c3e4f",
"phone_number_id": "4d3c2b1a-0f9e-4d8c-b7a6-958473625140",
"contact_id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"direction": "outbound",
"status": "completed",
"from": "+48732127980",
"to": "+48601234567",
"started_at": "2026-10-01T09:14:03Z",
"ended_at": "2026-10-01T09:17:41Z",
"duration_sec": 218,
"summary": "Jan added the company name and tax ID; asked for a call tomorrow at 10:00 about payments.",
"outcome": "message",
"sentiment": "positive",
"tags": [],
"has_recording": true,
"end_reason": "agent_hangup",
"callback_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"external_id": "gapli-evt-1842",
"outreach": {
"sequence_id": "11111111-1111-4111-8111-111111111111",
"enrollment_id": "8f7e6d5c-4b3a-4c2d-9e1f-0a9b8c7d6e5f",
"external_id": "gapli-evt-1842",
"step": 1
},
"caller_spoke": true,
"caller_words": 112
}Calendar, CRM and automation
Native connectors, no code needed
Google Calendar
The assistant checks real availability and books, moves and cancels appointments in your calendar. It only reads busy times, never event titles.
HubSpot
Every call is logged on the contact, bookings become meetings and callbacks become tasks. Optionally creates contacts and deals.
Pipedrive
Calls as activities or notes, bookings as meetings, callbacks as activities. Optionally creates people, deals or leads.
Zapier, Make and n8n
Step-by-step guides in the dashboard. Built on webhooks and the API, which are included in every plan.
Plans and limits
Which plan you need
Limits per account: requests per minute (also per key, sliding window) and per day (UTC). Above a limit you get 429 rate_limited with a Retry-After header.
Website voice widget: from the Office plan.
Need higher limits? Contact us and we can raise them for your account.
Early access: be among the first businesses to let AI answer the phone.
Ready to connect?
Start the free trial, create a test API key and send your first request in minutes. The API is included in every plan, from Line.