Skip to content

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.

API base URL
https://telofia.com/api/v1
curl · Test the key
curl https://telofia.com/api/v1/me \
  -H "Authorization: Bearer tf_live_…"
200 OK
{
  "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
  }
}

Quick start

Your first request in two minutes

  1. 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. 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. 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 · Test the key
curl https://telofia.com/api/v1/me \
  -H "Authorization: Bearer tf_live_…"
JavaScript: all booked calls, page by page
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 key

    Get the current account

    Returns the account the API key belongs to. Handy as a “test connection” call.

  • GET/assistantsScope: any key

    List 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 key

    Retrieve 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:assistants

    Update 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 key

    List phone numbers

    Your account's phone numbers with country, type, status and the assistant that answers them.

  • GET/callsScope: read:calls

    List calls

    Calls newest first, without transcripts. Demo calls are never included.

  • GET/calls/{id}Scope: read:calls

    Retrieve a call

    One call with the full transcript and the appointments booked during it.

  • GET/calls/{id}/recordingScope: read:calls

    Get 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:appointments

    List appointments

    Upcoming appointments in start order (from now by default).

  • GET/appointments/{id}Scope: read:appointments

    Retrieve an appointment

    One appointment of your account.

  • POST/appointments/{id}/cancelScope: write:appointments

    Cancel 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:appointments

    Reschedule 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:contacts

    List contacts

    Your callers, newest first. The assistant remembers them across calls.

  • GET/contacts/{id}Scope: read:contacts

    Retrieve a contact

    One contact with name, email, tags, notes and where the name and email came from (call, system or manual).

  • POST/contactsScope: write:contacts

    Create 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:contacts

    Update 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:tasks

    List tasks

    Callback requests, messages and other tasks from calls, newest first.

  • GET/tasks/{id}Scope: read:tasks

    Retrieve a task

    One task of your account.

  • PATCH/tasks/{id}Scope: write:tasks

    Update 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:calls

    Create 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:calls

    Retrieve 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:calls

    Cancel 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:messages

    List 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:messages

    Send 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 transcripts
  • read:appointmentsRead appointments
  • write:appointmentsCancel and reschedule appointments
  • read:contactsRead contacts
  • write:contactsCreate and update contacts
  • read:tasksRead tasks and callback requests
  • write:tasksUpdate task status
  • write:callsStart outbound calls, check and cancel them
  • read:messagesRead SMS messages and customer replies
  • write:messagesSend SMS to your contacts
  • read:assistantsRead the assistant's prompt (instructions, greeting, company profile)
  • write:assistantsChange the assistant's prompt

Errors

  • 400invalid_request — a parameter is missing or invalid
  • 401unauthorized — missing, invalid or revoked API key
  • 403insufficient_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 history
  • 404not_found — the object doesn't exist in your account
  • 409conflict — 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 header
  • 500server_error — something went wrong on our side
  • 503service_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 completed

    Summary, outcome, sentiment, transcript, contact and bookings; for outbound calls also the sequence or API request (callback_id, external_id, outreach).

  • call.startedCall started

    A call was answered (inbound, outbound or web).

  • appointment.bookedAppointment booked

    The assistant booked an appointment.

  • appointment.canceledAppointment canceled

    An appointment was canceled by phone or through the API.

  • appointment.rescheduledAppointment rescheduled

    An appointment was moved to a new time (includes the previous time).

  • task.createdCallback / task created

    A caller asked for a callback or left a task.

  • contact.createdNew contact

    A first-time caller was saved as a contact.

  • outbound_call.finishedOutbound call finished

    A 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.booked
  • Telofia-DeliveryUnique delivery ID
  • User-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.
Example delivery: call.completed
{
  "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.

Node.js · Verify the signature
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);
}
Python · Verify the signature
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", ""))
Example receiver (Express)
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.
Embed code (your key is in the dashboard)
<script src="https://telofia.com/widget.js" data-key="tfw_…" async></script>
JavaScript API
// 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 page

Strict 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.
Identity token
// 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

  • phonestringrequired
    Phone number, ideally in E.164 format. National numbers are read with your account's country code.
  • consenttruerequired
    Must be true: the person agreed to be contacted by phone, e.g. in your sign-up form.
  • namestringoptional
    Name, up to 120 characters.
  • emailstringoptional
    Email address.
  • contextstringoptional
    What the assistant should know for this person, up to 1,000 characters (e.g. the plan they chose).
  • external_idstring | numberoptional
    The person's ID in your system (string or number). The same ID is enrolled only once.
curl
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}'
201 Created
{
  "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.

  • external_id | enrollment_id | phonerequired
    Exactly one of: external_id (as sent when enrolling), enrollment_id (from the enroll response) or phone.
  • reasonoptional
    Optional reason, up to 200 characters, kept in your account's audit log.
  • 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
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"}'
200 OK
{
  "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.

call.completed for a sequence call
{
  "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.

PlanRequests / minRequests / dayAPI keysWebhooks
Free trial (test access)201,00011
Line302,00022
Desk6010,00055
Office18050,0001010
Network600200,0002520

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.