Swiftlee Answering REST API
v1 · JSON over HTTPS · bearer auth · HMAC-signed outbound webhooks
Base URL
https://www.getswiftlee.com/api/v1
The API answers on the same host as your portal — use the address you sign in at. A machine-readable OpenAPI 3.0 document lives at /api/v1/openapi.json — import it into Postman, Insomnia or a code generator.
Two kinds of keys
- Company keys (
ck_live_…; older keys start withsl_live_) — created in a client’s portal under Settings → API keys. Scoped to that one company:/me,/calls,/contacts,/sms,/hooks. - Operator keys (
ok_live_…; older keys start withop_live_) — for white-label operators managing many clients. Created in the admin console under API keys. Every call goes through/operator/companies/…and can only reach companies under your operator; a company outside it answers 403.
Company endpoints stay company-key-only — an operator key on /calls gets 403 wrong_key_kind. Use /operator/companies/:id/calls instead.
Authentication
All endpoints require a bearer token. Get a key from Settings → API keys inside the Swiftlee Answering portal. Keys look like ck_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx.
curl https://www.getswiftlee.com/api/v1/me \ -H "Authorization: Bearer ck_live_..."
The fallback header X-API-Key: ck_live_… also works for environments where Authorization headers are stripped by an upstream proxy.
Security: keys are shown ONCE on creation — we store only a SHA-256 hash. Lose it, revoke it, regenerate. Each key is scoped to a single company; cross-tenant access is impossible by construction.
Rate limits
100 requests per minute per key. 429 with Retry-After header on overage. If you need more, email support@getswiftlee.com.
Errors
Non-2xx responses share a stable shape:
{
"error": {
"code": "invalid_api_key",
"message": "API key not recognized."
}
}| Code | Status | When |
|---|---|---|
missing_api_key | 401 | no Authorization / X-API-Key header |
invalid_api_key_format | 401 | not a company key (ck_live_* / sl_live_*) or operator key (ok_live_* / op_live_*) |
invalid_api_key | 401 | key not recognized |
api_key_revoked | 401 | key was revoked |
wrong_key_kind | 403 | company key on an operator endpoint or vice versa |
forbidden | 403 | company is not under your operator |
not_found | 404 | no such company / row |
invalid_input | 400 | validation failed (details lists each field) |
conflict | 409 | duplicate or state conflict |
payment_required | 402 | number quota / billing blocks a purchase |
rate_limited | 429 | over 100 requests/min for the key (Retry-After set) |
upstream_failed | 502 | carrier / AI vendor call failed |
write_failed | 500 | database write failed |
Validation failures (invalid_input) also carry a details array of { path, message }.
Endpoints
/v1/meAuth smoke test — returns the company the key belongs to.
{
"company": {
"id": "c0d4...",
"name": "Acme Co",
"slug": "acme-co",
"status": "active",
"created_at": "2026-01-12T09:00:00Z",
"operator_id": "00000000-0000-0000-0000-000000000001"
},
"plan": { "slug": "growth", "name": "Growth", "included_calls": 200 },
"api_version": "v1"
}/v1/callsList calls for the authed company, newest first. Cursor-paginated.
limit— 1–100, default 25cursor— ISO timestamp; returns calls with started_at strictly before thisdisposition— answered | missed | voicemail | overflow | transferred | ai_handled
{
"data": [
{
"id": "ab12...",
"caller_e164": "+13105551234",
"started_at": "2026-05-25T14:32:11Z",
"ended_at": "2026-05-25T14:34:55Z",
"disposition": "answered",
"summary": "Caller asking about roof replacement quote.",
"transcript": "Hi, this is...",
"recording_available": true
}
],
"has_more": true,
"next_cursor": "2026-05-25T14:32:11Z"
}/v1/calls/:idFull detail for one call. 404 if the call isn't in the authed company.
Operator API — provisioning & configuration
Everything an operator’s team does in the admin console, as endpoints: create clients, set greetings and FAQs, escalation contacts, business hours, holidays, transfer destinations, intake forms and knowledge documents, buy numbers, resync the assistant, read calls. Each write lands on the same server-side functions the console uses, is audited with the key id, and resyncs only the client it touched.
| Method | Path (under /api/v1) | What it does |
|---|---|---|
| GET | /operator/me | Auth smoke test — the operator the key belongs to + client count. |
| GET | /operator/usage | Per-minute usage for a month: billable AI minutes per client (per call, rounded up to the whole minute; spam-excluded calls bill 0), the tier + rate that volume lands in, console seats, carrier pass-through (numbers, SMS, external transfer legs at cost + 15%) and the resulting line amounts. Read-only — no invoice is created. |
| GET | /operator/companies | List the operator's clients, newest first. Cursor-paginated on created_at. |
| POST | /operator/companies | Create a client under your operator (same path as the admin “Add client” form). |
| GET | /operator/companies/{id} | One client. 404 unless it is under your operator. |
| PATCH | /operator/companies/{id} | Update profile + routing. Only the fields you send change. |
| PUT | /operator/companies/{id}/greeting | Set the AI/receptionist greeting (EN and/or ES). Resyncs this company's assistant. |
| PUT | /operator/companies/{id}/instructions | Set AI custom instructions and do-nots. Resyncs this company's assistant. |
| PUT | /operator/companies/{id}/faqs | Replace the FAQ list (max 20). Resyncs this company's assistant. GET returns the current list. |
| PUT | /operator/companies/{id}/escalation-contacts | Replace escalation contacts (max 10, optional on-call schedule). GET returns the current list. |
| PUT | /operator/companies/{id}/business-hours | Replace the weekly schedule (send all 7 days) + optional language / after-hours mode / retention. GET returns the current values. |
| PUT | /operator/companies/{id}/holidays | Replace the holiday calendar (dates not sent are removed) + optional overflow mode. GET returns the current calendar. |
| PUT | /operator/companies/{id}/transfer-destinations | Replace external transfer destinations (US/CA numbers or sip: URIs) and optionally enable the feature. Resyncs when enabled. GET returns the current list. |
| PUT | /operator/companies/{id}/intake-form | Create (no id) or update (id) an intake form. Resyncs this company's assistant. GET lists the forms. |
| PUT | /operator/companies/{id}/scheduling | Replace the scheduling rules model (W8): scheduler connections (upserted — secrets are write-only), locations, services, providers with matching criteria, and confirmation settings. Locations / services / providers not sent are removed. Resyncs the assistant. GET returns the current model without secrets. |
| PUT | /operator/companies/{id}/intake-flow | Replace the default intake form as an ordered, branching step list the AI runs before booking (W8). `when` skips a step unless an earlier step's answer matches. Resyncs this company's assistant. GET returns the current flow (?form=<id> for another form). |
| GET | /operator/companies/{id}/knowledge | List knowledge-base documents (title, source, digest, include_in_prompt). |
| POST | /operator/companies/{id}/knowledge | Add a document: multipart `file` (PDF/DOCX/TXT/MD/CSV/HTML, ≤10 MB) or JSON `{ url }` to crawl a website (sitemap first, ≤20 pages). Condensed, stored, assistant resynced. |
| GET | /operator/companies/{id}/numbers | The company's phone numbers. |
| POST | /operator/companies/{id}/numbers$ | Search + buy a number. SPENDS MONEY. First number runs full provisioning (number + AI leg); later calls add a line. |
| POST | /operator/companies/{id}/resync | Push the company's current scripts, FAQs, knowledge, intake and transfer config to its live assistant (this company only). |
| GET | /operator/companies/{id}/calls | Calls for one client — same shape and params as the company-key GET /calls. |
| PUT | /operator/companies/{id}/tools | Replace the client's custom tools — functions the AI can call mid-call against the client's own systems through our proxy (credentials stay with us, never with the AI vendor). Matched by id, else by name; tools not sent are removed. GET returns the list with has_secret (never the secret). |
| PUT | /operator/companies/{id}/verification-codes | Verification-code forwarding (W13): when a one-time code arrives on one of the client's lines — by text, or read out by an automated caller — forward it to the client's own people by SMS from that line and/or by email, keep a MASKED record for 24 h, then purge. Off by default. Enabling is refused (402) for trial / lead / lapsed self-serve companies; wholesale-billed operators' clients qualify. Flipping the flag resyncs this company's assistant (adds/removes the capture_verification_code tool). GET returns the settings, recipients and the masked last-24-hours ledger. |
| PUT | /operator/companies/{id}/privacy | Sensitive-data controls (W11): which designated data kinds are redacted from transcripts, summaries, intake, live transcript, notifications, webhooks, CRM / Sheets / Slack pushes and recording deliveries; whether AI-handled calls are recorded at all; whether the recording pauses while a sensitive intake field is collected. Keys omitted keep their current value. GET returns the policy plus what it resolves to. |
Typical onboarding in five calls
POST /operator/companieswith anIdempotency-Key→ save the returnedcompany.idPUT …/greeting,PUT …/faqs,PUT …/escalation-contacts,PUT …/business-hoursPOST …/knowledgewith{ "url": "https://client-site.com" }POST …/numbers(buys the line; the first number also builds the assistant)POST …/resynconce, thenGET …/callsas calls arrive
/v1/operator/meAuth smoke test — the operator the key belongs to + client count.
curl -X GET https://getswiftlee.com/api/v1/operator/me \ -H "Authorization: Bearer ok_live_…"
{
"operator": {
"id": "9b1d0c2e-1111-4a3b-8c9d-0e1f2a3b4c5d",
"slug": "conversational",
"name": "Conversational",
"active": true
},
"companies": 112,
"scopes": [],
"api_version": "v1"
}/v1/operator/usagePer-minute usage for a month: billable AI minutes per client (per call, rounded up to the whole minute; spam-excluded calls bill 0), the tier + rate that volume lands in, console seats, carrier pass-through (numbers, SMS, external transfer legs at cost + 15%) and the resulting line amounts. Read-only — no invoice is created.
month— YYYY-MM, default = current month
curl -X GET https://getswiftlee.com/api/v1/operator/usage \ -H "Authorization: Bearer ok_live_…"
{
"month": "2026-10",
"operator": {
"id": "9b1d0c2e-1111-4a3b-8c9d-0e1f2a3b4c5d",
"slug": "conversational",
"billing_mode": "per_minute"
},
"period_start": "2026-10-01",
"period_end": "2026-11-01",
"currency": "usd",
"total_cents": 1044693,
"lines": [
{
"kind": "ai_minutes",
"description": "AI minutes — October 2026 · 31,240 min @ $0.32/min (tier ≥ 25,000 min)",
"quantity": 31240,
"unit_cents": 32,
"amount_cents": 999680
},
{
"kind": "seats",
"description": "Console seats — 2 concurrent × $149.00 — October 2026",
"quantity": 2,
"unit_cents": 14900,
"amount_cents": 29800
},
{
"kind": "passthrough_numbers",
"description": "Phone numbers — 112 × $1.1500/mo (carrier +15%)",
"quantity": 112,
"unit_cents": 115,
"amount_cents": 12880
},
{
"kind": "passthrough_sms",
"description": "SMS — 1,240 messages × $0.0115 (carrier +15%)",
"quantity": 1240,
"unit_cents": 1.15,
"amount_cents": 1426
},
{
"kind": "passthrough_transfer_legs",
"description": "External transfer legs — 480 legs, 1,120 min × $0.0081/min (carrier +15%)",
"quantity": 1120,
"unit_cents": 0.81,
"amount_cents": 907
}
],
"ai_minutes": {
"total_minutes": 31240,
"total_seconds": 1612380,
"tier": {
"min_minutes": 25000,
"cents_per_min": 32
},
"calls": 16410,
"excluded_calls": 212,
"estimated_calls": 3,
"by_client": [
{
"company_id": "3f9c2a1e-7d4b-4c0e-9a11-5d2b8e6f1a20",
"name": "ABC Plumbing",
"calls": 402,
"billable_calls": 388,
"excluded_calls": 6,
"seconds": 41210,
"minutes": 812
}
]
},
"seats": {
"purchased": 2,
"billed": 2,
"per_seat_cents": 14900
},
"warnings": [],
"generated_at": "2026-10-14T18:02:11.000Z"
}Errors: 400 invalid_input — month is not YYYY-MM or is in the future · 500 usage_unavailable — usage could not be computed (transient — retry)
/v1/operator/companiesList the operator's clients, newest first. Cursor-paginated on created_at.
limit— 1–100, default 25cursor— ISO created_at of the last row from the previous pagestatus— lead | trial | active | past_due | paused | cancelled
curl -X GET https://getswiftlee.com/api/v1/operator/companies \ -H "Authorization: Bearer ok_live_…"
{
"data": [
{
"id": "3f9c2a1e-7d4b-4c0e-9a11-5d2b8e6f1a20",
"operator_id": "9b1d0c2e-1111-4a3b-8c9d-0e1f2a3b4c5d",
"name": "ABC Plumbing",
"slug": "abc-plumbing",
"status": "active",
"provisioning_status": "live",
"routing_mode": "hybrid",
"plan": {
"slug": "growth",
"name": "Growth"
},
"primary_contact_name": "Dana Ruiz",
"primary_contact_email": "dana@abcplumbing.com",
"primary_contact_phone": "+16195550142",
"industry": "plumbing",
"timezone": "America/Los_Angeles",
"language_pref": "en_es",
"after_hours_mode": "ai",
"hipaa_mode": false,
"has_assistant": true,
"created_at": "2026-09-29T18:04:11.000Z"
}
],
"has_more": false,
"next_cursor": null
}/v1/operator/companiesCreate a client under your operator (same path as the admin “Add client” form).
plan_slug must be an active plan in your operator catalog. status defaults to active; trial seeds a 14-day window.
Idempotency-Key— Optional. Replaying the same key returns the company created the first time (200, replayed:true) instead of a duplicate.
curl -X POST https://getswiftlee.com/api/v1/operator/companies \
-H "Authorization: Bearer ok_live_…" \
-H "Idempotency-Key: crm-account-88213" \
-H "Content-Type: application/json" \
-d '{"name":"ABC Plumbing","primary_contact_email":"dana@abcplumbing.com","primary_contact_name":"Dana Ruiz","primary_contact_phone":"+16195550142","plan_slug":"growth","status":"active","invite_owner":false}'{
"company": {
"id": "3f9c2a1e-7d4b-4c0e-9a11-5d2b8e6f1a20",
"operator_id": "9b1d0c2e-1111-4a3b-8c9d-0e1f2a3b4c5d",
"name": "ABC Plumbing",
"slug": "abc-plumbing",
"status": "active",
"provisioning_status": "live",
"routing_mode": "hybrid",
"plan": {
"slug": "growth",
"name": "Growth"
},
"primary_contact_name": "Dana Ruiz",
"primary_contact_email": "dana@abcplumbing.com",
"primary_contact_phone": "+16195550142",
"industry": "plumbing",
"timezone": "America/Los_Angeles",
"language_pref": "en_es",
"after_hours_mode": "ai",
"hipaa_mode": false,
"has_assistant": true,
"created_at": "2026-09-29T18:04:11.000Z"
},
"replayed": false
}Errors: 400 invalid_input — missing/invalid field or unknown plan_slug · 409 conflict — a client with that primary_contact_email already exists under your operator
/v1/operator/companies/{id}One client. 404 unless it is under your operator.
curl -X GET https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID \ -H "Authorization: Bearer ok_live_…"
{
"company": {
"id": "3f9c2a1e-7d4b-4c0e-9a11-5d2b8e6f1a20",
"operator_id": "9b1d0c2e-1111-4a3b-8c9d-0e1f2a3b4c5d",
"name": "ABC Plumbing",
"slug": "abc-plumbing",
"status": "active",
"provisioning_status": "live",
"routing_mode": "hybrid",
"plan": {
"slug": "growth",
"name": "Growth"
},
"primary_contact_name": "Dana Ruiz",
"primary_contact_email": "dana@abcplumbing.com",
"primary_contact_phone": "+16195550142",
"industry": "plumbing",
"timezone": "America/Los_Angeles",
"language_pref": "en_es",
"after_hours_mode": "ai",
"hipaa_mode": false,
"has_assistant": true,
"created_at": "2026-09-29T18:04:11.000Z"
}
}/v1/operator/companies/{id}Update profile + routing. Only the fields you send change.
Fields: name, plan_id, primary_contact_name/email/phone, industry, timezone, language_pref (en|es|en_es), routing_mode (ai|human|hybrid — a change reconciles telephony), after_hours_mode (ai|live_overflow|voicemail), record_inbound_calls, record_outbound_calls, hold_music_url (https). `{id}` is the company id (uuid) from POST/GET /operator/companies.
curl -X PATCH https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"name":"ABC Plumbing & Heating","timezone":"America/Los_Angeles","language_pref":"en_es","routing_mode":"hybrid","after_hours_mode":"ai"}'{
"company": {
"id": "3f9c2a1e-7d4b-4c0e-9a11-5d2b8e6f1a20",
"operator_id": "9b1d0c2e-1111-4a3b-8c9d-0e1f2a3b4c5d",
"name": "ABC Plumbing",
"slug": "abc-plumbing",
"status": "active",
"provisioning_status": "live",
"routing_mode": "hybrid",
"plan": {
"slug": "growth",
"name": "Growth"
},
"primary_contact_name": "Dana Ruiz",
"primary_contact_email": "dana@abcplumbing.com",
"primary_contact_phone": "+16195550142",
"industry": "plumbing",
"timezone": "America/Los_Angeles",
"language_pref": "en_es",
"after_hours_mode": "ai",
"hipaa_mode": false,
"has_assistant": true,
"created_at": "2026-09-29T18:04:11.000Z"
},
"stripe_warning": false,
"routing_warning": null
}/v1/operator/companies/{id}/greetingSet the AI/receptionist greeting (EN and/or ES). Resyncs this company's assistant.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/greeting \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"greeting_en":"Thanks for calling ABC Plumbing, this is Mia. How can I help?","greeting_es":"Gracias por llamar a ABC Plumbing, habla Mia. ¿En qué puedo ayudarle?"}'{
"ok": true,
"warning": null
}/v1/operator/companies/{id}/instructionsSet AI custom instructions and do-nots. Resyncs this company's assistant.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/instructions \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"instructions":"Always collect the service address before offering a time.","do_not_do":"Never quote prices."}'{
"ok": true,
"warning": null
}/v1/operator/companies/{id}/faqsReplace the FAQ list (max 20). Resyncs this company's assistant. GET returns the current list.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/faqs \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"faqs":[{"question":"Do you offer emergency service?","answer":"Yes — 24/7, with a $150 after-hours dispatch fee."}]}'{
"faqs": [
{
"question": "Do you offer emergency service?",
"answer": "Yes — 24/7, with a $150 after-hours dispatch fee."
}
],
"warning": null
}/v1/operator/companies/{id}/escalation-contactsReplace escalation contacts (max 10, optional on-call schedule). GET returns the current list.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/escalation-contacts \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"contacts":[{"name":"Dana Ruiz","role":"Owner","phone":"+16195550142","email":"dana@abcplumbing.com","conditions":"Emergencies and complaints","days":["mon","tue","wed","thu","fri"],"start":"08:00","end":"18:00"}]}'{
"contacts": [
{
"name": "Dana Ruiz",
"role": "Owner",
"phone": "+16195550142",
"email": "dana@abcplumbing.com",
"conditions": "Emergencies and complaints",
"days": [
"mon",
"tue",
"wed",
"thu",
"fri"
],
"start": "08:00",
"end": "18:00"
}
]
}/v1/operator/companies/{id}/business-hoursReplace the weekly schedule (send all 7 days) + optional language / after-hours mode / retention. GET returns the current values.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/business-hours \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"hours":[{"day":"mon","open":true,"start":"08:00","end":"18:00"},{"day":"tue","open":true,"start":"08:00","end":"18:00"},{"day":"wed","open":true,"start":"08:00","end":"18:00"},{"day":"thu","open":true,"start":"08:00","end":"18:00"},{"day":"fri","open":true,"start":"08:00","end":"17:00"},{"day":"sat","open":false},{"day":"sun","open":false}],"language_pref":"en_es","after_hours_mode":"ai","recording_retention_days":90}'{
"hours": {
"mon": {
"open": true,
"start": "08:00",
"end": "18:00"
},
"sat": {
"open": false,
"start": "",
"end": ""
}
}
}Errors: 400 invalid_input — an open day's end is not after its start (#139)
/v1/operator/companies/{id}/holidaysReplace the holiday calendar (dates not sent are removed) + optional overflow mode. GET returns the current calendar.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/holidays \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"holidays":[{"date":"2026-12-25","name":"Christmas Day","mode":"closed"},{"date":"2026-12-24","name":"Christmas Eve","mode":"custom","custom_greeting":"We close at noon today."}],"after_no_agent_mode":"ai"}'{
"holidays": [
{
"id": "…",
"date": "2026-12-24",
"name": "Christmas Eve",
"mode": "custom",
"custom_greeting": "We close at noon today."
},
{
"id": "…",
"date": "2026-12-25",
"name": "Christmas Day",
"mode": "closed",
"custom_greeting": null
}
]
}/v1/operator/companies/{id}/transfer-destinationsReplace external transfer destinations (US/CA numbers or sip: URIs) and optionally enable the feature. Resyncs when enabled. GET returns the current list.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/transfer-destinations \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"enabled":true,"destinations":[{"name":"Front desk","kind":"pstn","target":"+16195550100","priority":10,"announce":true,"require_accept":true},{"name":"PBX","kind":"sip","target":"sip:reception@pbx.example.com","priority":20,"pass_sip_headers":true}]}'{
"enabled": true,
"destinations": [
{
"id": "…",
"name": "Front desk",
"kind": "pstn",
"target": "+16195550100",
"priority": 10,
"hours": null,
"announce": true,
"require_accept": true,
"pass_sip_headers": true,
"active": true
}
],
"warning": null
}/v1/operator/companies/{id}/intake-formCreate (no id) or update (id) an intake form. Resyncs this company's assistant. GET lists the forms.
Field types: text, longtext, phone, email, number, date, select, yesno (see the builder). Field ids are slugged from labels when omitted.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/intake-form \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"name":"New caller intake","applies_to":"new","is_default":true,"fields":[{"label":"Full name","type":"text","required":true},{"label":"Service address","type":"longtext","required":true},{"label":"Issue type","type":"select","required":true,"options":["Leak","No hot water","Clog","Other"]},{"label":"Is water actively leaking?","type":"yesno","required":false,"showIf":{"fieldId":"issue_type","equals":"Leak"}}]}'{
"form": {
"id": "…",
"name": "New caller intake",
"applies_to": "new",
"is_default": true,
"fields": [
{
"id": "full_name",
"label": "Full name",
"type": "text",
"required": true
}
]
},
"warning": null
}/v1/operator/companies/{id}/schedulingReplace the scheduling rules model (W8): scheduler connections (upserted — secrets are write-only), locations, services, providers with matching criteria, and confirmation settings. Locations / services / providers not sent are removed. Resyncs the assistant. GET returns the current model without secrets.
Kinds: google (uses the company's Google Calendar OAuth from Integrations; calendarRef = calendar id), calcom (secret = API key; calendarRef = event type id), acuity (config.userId + secret = API key; calendarRef = calendar id, serviceRefs {serviceId: appointmentTypeID}), rest (config.baseUrl + optional paths — see docs/scheduling-rest-connector.md), m365 (needs platform setup). connectionId / serviceIds / locationIds accept ids or names. Placeholders for confirmation templates: {{name}} {{when}} {{provider}} {{service}} {{location}} {{address}} {{business}}.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/scheduling \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"connections":[{"label":"Front desk scheduler","kind":"rest","config":{"baseUrl":"https://scheduler.example.com/api","authHeaderName":"Authorization"},"secret":"Bearer sk_…"}],"locations":[{"name":"North Park","address":"123 Main St, San Diego"},{"name":"Chula Vista"}],"services":[{"name":"Cleaning","durationMin":60},{"name":"Pediatric exam","durationMin":30}],"providers":[{"name":"Dr. Alvarez","connectionId":"Front desk scheduler","calendarRef":"dr_alvarez","locationIds":["North Park"],"minAge":18,"insuranceAccepted":["Delta Dental","Aetna"],"acceptsSelfPay":true,"priority":10},{"name":"Dr. Kim","connectionId":"Front desk scheduler","calendarRef":"dr_kim","serviceIds":["Pediatric exam","Cleaning"],"maxAge":17,"acceptsInsurance":true,"acceptsSelfPay":false,"priority":20}],"settings":{"rulesText":"New patients need 15 extra minutes.","confirmationEmailEnabled":true,"infoEmailEnabled":true,"infoEmailTo":"frontdesk@example.com"}}'{
"configured": true,
"connections": [
{
"id": "…",
"kind": "rest",
"label": "Front desk scheduler",
"status": "active",
"hasSecret": true
}
],
"locations": [
{
"id": "…",
"name": "North Park"
}
],
"services": [
{
"id": "…",
"name": "Cleaning",
"durationMin": 60
}
],
"providers": [
{
"id": "…",
"name": "Dr. Alvarez",
"connectionId": "…",
"calendarRef": "dr_alvarez"
}
],
"settings": {
"rulesText": "New patients need 15 extra minutes."
},
"warning": null
}/v1/operator/companies/{id}/intake-flowReplace the default intake form as an ordered, branching step list the AI runs before booking (W8). `when` skips a step unless an earlier step's answer matches. Resyncs this company's assistant. GET returns the current flow (?form=<id> for another form).
Step ids are slugged from `field` when omitted; `when.field` must reference an EARLIER step. `ask` is how the AI phrases the question (stored as the field's aiHint). Under hipaa_mode the answers stay in the portal (never in SMS / email / webhooks).
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/intake-flow \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"steps":[{"id":"patient_name","field":"Patient name","type":"text","required":true},{"id":"insurance_or_self_pay","field":"Insurance or self-pay","ask":"Will you be using insurance, or paying yourself?","type":"select","required":true,"options":["insurance","self-pay"]},{"id":"carrier","field":"Insurance carrier","type":"text","required":true,"when":{"field":"insurance_or_self_pay","equals":"insurance"}},{"id":"member_id","field":"Member ID","type":"text","required":false,"when":{"field":"insurance_or_self_pay","in":["insurance"]}}]}'{
"form_id": "…",
"name": "Default intake",
"steps": [
{
"id": "patient_name",
"field": "Patient name",
"ask": "What is the patient name?",
"type": "text",
"required": true
}
],
"warning": null
}/v1/operator/companies/{id}/knowledgeList knowledge-base documents (title, source, digest, include_in_prompt).
curl -X GET https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/knowledge \ -H "Authorization: Bearer ok_live_…"
{
"documents": [
{
"id": "…",
"title": "Website — abcplumbing.com",
"source_type": "website",
"source_ref": "https://abcplumbing.com",
"status": "ready",
"include_in_prompt": true,
"digest": "ABC Plumbing serves San Diego County…"
}
]
}/v1/operator/companies/{id}/knowledgeAdd a document: multipart `file` (PDF/DOCX/TXT/MD/CSV/HTML, ≤10 MB) or JSON `{ url }` to crawl a website (sitemap first, ≤20 pages). Condensed, stored, assistant resynced.
For a file: `-F file=@pricing.pdf -F title=Pricing`. Max 25 documents per company.
curl -X POST https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/knowledge \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"url":"https://abcplumbing.com","title":"Website"}'{
"document": {
"id": "…",
"title": "Website",
"source_type": "website",
"status": "ready",
"include_in_prompt": true
},
"pages": 12,
"warning": null
}Errors: 409 conflict — the company already holds 25 documents
/v1/operator/companies/{id}/numbersThe company's phone numbers.
curl -X GET https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/numbers \ -H "Authorization: Bearer ok_live_…"
{
"numbers": [
{
"id": "…",
"e164": "+18445550123",
"number_type": "main",
"active": true,
"provisioning_status": "live",
"porting_status": "not_applicable",
"created_at": "2026-09-29T18:04:11.000Z"
}
]
}/v1/operator/companies/{id}/numbersSPENDS MONEYSearch + buy a number. SPENDS MONEY. First number runs full provisioning (number + AI leg); later calls add a line.
Omit area_code for a toll-free number. number_type: main | after_hours | tracking.
curl -X POST https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/numbers \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"area_code":"619","number_type":"main"}'{
"e164": "+16195550123",
"first_number": true,
"status": "live"
}Errors: 402 payment_required — the company's number quota or billing state blocks the purchase (nothing is bought) · 502 upstream_failed — carrier search/purchase failed
/v1/operator/companies/{id}/resyncPush the company's current scripts, FAQs, knowledge, intake and transfer config to its live assistant (this company only).
curl -X POST https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/resync \ -H "Authorization: Bearer ok_live_…"
{
"ok": true,
"assistant_id": "asst_…"
}Errors: 409 conflict — the company has no assistant (human-tier without the after-hours add-on, or not provisioned)
/v1/operator/companies/{id}/callsCalls for one client — same shape and params as the company-key GET /calls.
limit— 1–100, default 25cursor— ISO timestamp; returns calls with started_at strictly before thisdisposition— answered | missed | voicemail | overflow | transferred | ai_handled
curl -X GET https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/calls \ -H "Authorization: Bearer ok_live_…"
{
"data": [
{
"id": "ab12c3d4-0000-4000-8000-000000000001",
"caller_e164": "+13105551234",
"started_at": "2026-09-25T14:32:11Z",
"ended_at": "2026-09-25T14:34:55Z",
"disposition": "answered",
"summary": "Caller asking about a water-heater replacement quote.",
"transcript": "Hi, this is…",
"recording_available": true
}
],
"has_more": true,
"next_cursor": "2026-09-25T14:32:11Z"
}/v1/operator/companies/{id}/toolsReplace the client's custom tools — functions the AI can call mid-call against the client's own systems through our proxy (credentials stay with us, never with the AI vendor). Matched by id, else by name; tools not sent are removed. GET returns the list with has_secret (never the secret).
url must be public https. {{param}} placeholders are URL-encoded in url and JSON-escaped in body_template (wrap them in quotes); built-ins: {{caller_phone}}, {{called_number}}, {{call_id}}. auth_kind: none | bearer | basic (secret = user:pass) | header (+ auth_header_name). response_path is a dotted path into the JSON reply; response_template uses {{value}} / {{value.field}}.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/tools \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"tools":[{"name":"lookup_order","description":"Look up an order by its order number and report its status and delivery date.","parameters":{"type":"object","properties":{"order_number":{"type":"string","description":"The order number the caller gives"}},"required":["order_number"]},"method":"GET","url":"https://api.client.com/orders/{{order_number}}","headers":{"Accept":"application/json"},"auth_kind":"bearer","auth_secret":"sk_live_…","response_path":"data.order","response_template":"Order {{value.id}} is {{value.status}} and ships {{value.eta}}.","timeout_ms":6000,"max_calls_per_conversation":5,"active":true}]}'{
"tools": [
{
"id": "…",
"name": "lookup_order",
"description": "Look up an order by its order number and report its status and delivery date.",
"parameters": {
"type": "object",
"properties": {
"order_number": {
"type": "string",
"description": "The order number the caller gives"
}
},
"required": [
"order_number"
]
},
"method": "GET",
"url": "https://api.client.com/orders/{{order_number}}",
"headers": {
"Accept": "application/json"
},
"auth_kind": "bearer",
"auth_header_name": null,
"has_secret": true,
"body_template": null,
"response_path": "data.order",
"response_template": "Order {{value.id}} is {{value.status}} and ships {{value.eta}}.",
"timeout_ms": 6000,
"max_calls_per_conversation": 5,
"active": true
}
],
"warning": null
}/v1/operator/companies/{id}/verification-codesVerification-code forwarding (W13): when a one-time code arrives on one of the client's lines — by text, or read out by an automated caller — forward it to the client's own people by SMS from that line and/or by email, keep a MASKED record for 24 h, then purge. Off by default. Enabling is refused (402) for trial / lead / lapsed self-serve companies; wholesale-billed operators' clients qualify. Flipping the flag resyncs this company's assistant (adds/removes the capture_verification_code tool). GET returns the settings, recipients and the masked last-24-hours ledger.
via: subset of sms | email (SMS goes out from the line that received the code, no marketing footer). recipients (max 10, optional — omit to keep the current list): each needs a phone or an email. With no recipients, codes are recorded masked but not forwarded — unless an escalation contact's conditions mention verification codes.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/verification-codes \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"enabled":true,"via":["sms","email"],"recipients":[{"name":"Office manager","phone":"+16195550142","email":"office@abcplumbing.com","active":true}]}'{
"available": true,
"enabled": true,
"via": [
"sms",
"email"
],
"eligible": true,
"status": "active",
"recipients": [
{
"id": "…",
"name": "Office manager",
"phone": "+16195550142",
"email": "office@abcplumbing.com",
"active": true
}
],
"warning": null
}Errors: 402 payment_required — enabling for a company that is not active / not under a wholesale-billed operator
/v1/operator/companies/{id}/privacySensitive-data controls (W11): which designated data kinds are redacted from transcripts, summaries, intake, live transcript, notifications, webhooks, CRM / Sheets / Slack pushes and recording deliveries; whether AI-handled calls are recorded at all; whether the recording pauses while a sensitive intake field is collected. Keys omitted keep their current value. GET returns the policy plus what it resolves to.
redact_fields values: ssn | card | dob | bank_account | drivers_license | passport | custom (custom requires redact_custom_patterns: up to 10 { label, regex } — ≤200 chars, no backreferences / nested quantifiers). Redacted values become [SSN], [CARD], [DOB], [BANK_ACCOUNT], [DRIVERS_LICENSE], [PASSPORT], [LABEL]; spoken digits ("four one one one…") are detected too. card also turns on transcription-time PCI redaction. ai_recording_enabled=false → no vendor recording, no caller-leg recording, no consent line on AI calls. recording_pause_sensitive=true → the AI calls begin_/end_sensitive_capture around fields marked sensitive on the intake form (and the policy's kinds); the vendor recording is switched off and the pausable carrier recording becomes the stored one.
curl -X PUT https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/privacy \
-H "Authorization: Bearer ok_live_…" \
-H "Content-Type: application/json" \
-d '{"redact_fields":["ssn","card","dob","custom"],"redact_custom_patterns":[{"label":"Member ID","regex":"\\bMBR-\\d{6}\\b"}],"recording_pause_sensitive":true,"ai_recording_enabled":true,"record_inbound_calls":false}'{
"policy": {
"available": true,
"redact_fields": [
"ssn",
"card",
"dob",
"custom"
],
"redact_custom_patterns": [
{
"label": "Member ID",
"regex": "\\bMBR-\\d{6}\\b"
}
],
"recording_pause_sensitive": true,
"ai_recording_enabled": true,
"record_inbound_calls": false,
"sensitive_fields": [
{
"id": "card_number",
"label": "Card number",
"form_id": "…"
}
],
"effective": {
"ai_calls": {
"vapi": false,
"telnyxCallerLeg": true,
"anyRecording": true,
"canonical": "telnyx"
},
"human_calls_recorded": false,
"disclosure_spoken_on_ai_calls": true,
"transcriber_redaction": [
"pci"
],
"pause_tools_registered": true
}
},
"warning": null
}Custom tools (client systems)
A custom tool is a function the AI can call during a call against one of the client’s own systems — look up an order, check a ticket, create a lead — so one conversation can touch several applications. You describe the request once; the AI fills in the parameters from what the caller says and tells them the outcome in plain words. Every call goes through our proxy: the client’s credential is stored encrypted on our side, the request is signed and sent from here with a hard timeout, and the voice AI vendor only ever sees the proxy URL. Manage tools in the portal under Settings → Client systems, on the admin company page, or with PUT /operator/companies/:id/tools.
Worked example — “where’s my order 4521?”
{
"tools": [{
"name": "lookup_order",
"description": "Look up an order by its order number and report its status and delivery date.",
"parameters": {
"type": "object",
"properties": { "order_number": { "type": "string", "description": "The order number the caller gives" } },
"required": ["order_number"]
},
"method": "GET",
"url": "https://api.client.com/orders/{{order_number}}",
"auth_kind": "bearer",
"auth_secret": "sk_live_…",
"response_path": "data.order",
"response_template": "Order {{value.id}} is {{value.status}} and ships {{value.eta}}."
}]
}- The caller says “where’s my order 4521?” → the AI calls
lookup_order(order_number: "4521"). - Our proxy renders
GET https://api.client.com/orders/4521withAuthorization: Bearer sk_live_…, times out after 6s, follows no redirects. - The reply
{ "data": { "order": { "id": "4521", "status": "packed", "eta": "Tuesday" } } }is narrowed byresponse_path, thenresponse_templatebecomes what the AI is told: “Order 4521 is packed and ships Tuesday.” - The AI speaks that to the caller. The call’s invocation log records the tool, status, duration and a redacted summary (status only for HIPAA-mode clients).
Rules: https and public hosts only (private, loopback and cloud-metadata addresses are refused at save time and again at call time); {{param}} placeholders are URL-encoded in the URL and JSON-escaped in body_template (wrap them in quotes); built-ins {{caller_phone}}, {{called_number}}, {{call_id}}; auth_kind is none | bearer | basic (secret =user:pass) | header (+ auth_header_name); at most 20 tools per client and max_calls_per_conversation uses of each per call. Non-2xx replies and timeouts become a safe “I couldn’t reach that system” for the AI — never an error the caller hears verbatim.
Privacy controls
Per client, three controls keep designated data out of everything the platform records, transcribes, stores or transmits. Manage them in the portal under Settings → Privacy, on the admin company page, or with PUT /operator/companies/:id/privacy.
- Redaction (
redact_fields) —ssn,card,dob,bank_account,drivers_license,passportand up to 10custompatterns. A matching value is replaced by a token ([SSN],[CARD],[DOB], …) before it is stored or sent anywhere: the transcript and summary, structured intake, the receptionist’s live transcript, escalation SMS / email / push,call.completed/message.capturedpayloads, CRM / Sheets / Slack pushes and recording deliveries. Card numbers are Luhn-checked; spoken digits (“four one one one…”) count; phone numbers, years, order numbers and appointment dates are left alone. Withcardon, the transcription engine also redacts PCI data before the model sees it. Intake fields marked Sensitive on the intake form are removed wholesale. - Recording on/off for AI calls (
ai_recording_enabled) — off means no vendor recording, no carrier recording and no “this call may be recorded” line on that client’s AI calls. Calls answered by receptionists keep their own flag (record_inbound_calls). - Pause while collecting sensitive details (
recording_pause_sensitive) — the AI brackets each sensitive field (intake fields marked Sensitive, plus the redaction kinds) withbegin_sensitive_capture/end_sensitive_capture, which pause and resume the carrier recording; markers land in the call’srecording_paused_segmentsand a “recording paused” status shows on the live view. Because the vendor recording cannot be paused, it is switched off for that client and the pausable carrier recording of the bridged caller leg (both sides) is the one stored and delivered. A pause the AI never closed is closed at the end of the call.
Webhooks
The richer integration pattern: subscribe to events at Settings → Webhooks and Swiftlee Answering POSTs to your endpoint as events happen — no polling.
| Event | Fires when |
|---|---|
message.captured | A message was taken for the client (post-call workflow). |
call.completed | Every finished call, any disposition. |
call.qualified | Answered call with urgency urgent/emergency. |
call.disqualified | Answered call with routine urgency. |
call.voicemail | Caller left a voicemail. |
call.no_answer | Missed call (partner-integration name). |
call.missed | Legacy alias of call.no_answer; fires at the same point. |
callback.queued | A callback was queued for a receptionist. |
appointment.booked | The assistant booked an appointment (payload adds booking_ref, provider, service, location; notes withheld under hipaa_mode). |
appointment.cancelled | The caller cancelled a booking by phone (W8) — booking_ref, starts_at, provider, reason (withheld under hipaa_mode). |
appointment.rescheduled | The caller moved a booking by phone (W8) — booking_ref, previous_starts_at, starts_at, ends_at, provider. |
call.escalated | A live caller was handed to one of the client's transfer destinations — payload carries caller, reason, intake so far and the destination. |
Signature: X-Webhook-Signature: t=<unix>,v1=<hex> — HMAC-SHA256 of ${t}.${rawBody} with your whsec_ signing secret. Reject if t is more than 5 minutes stale (replay defense). Every delivery also carries X-Webhook-Event (the event type) and X-Webhook-Delivery (a delivery id). Receivers built before 2026-09-29 may still read the identical legacy X-Swiftlee-* twins; those are removed on 2026-12-28.
// Node.js verification snippet
import crypto from "node:crypto";
function verifyWebhookSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
const t = parts.t;
const v1 = parts.v1;
if (!t || !v1) return false;
const age = Math.floor(Date.now() / 1000) - Number(t);
if (age > 300 || age < -60) return false; // replay window
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(v1, "hex"),
);
}Recordings storage
Call audio is stored as a private object and is only ever handed out as a signed URL that expires — never a bucket path. Three ways to get it:
GET /v1/calls/:id/recording— 302 to a 1-hour signed URL. Add?format=jsonfor{ url, expires_at, content_type }, and?ttl=(60 … 604800 seconds) to choose the lifetime. Operator keys may fetch calls of companies under their operator.- Webhook opt-in — turn on “Include recording + transcript in webhook events” (Settings → Recordings).
call.completedandmessage.capturedthen also carryrecording_url(signed, 1 hour),recording_expires_atandtranscript. Off by default: existing payloads are unchanged unless you opt in. - Push destinations — every new recording (with transcript + summary) is POSTed to an https endpoint you register, signed with
X-Webhook-Signature: t=<unix>,v1=<hex>(same HMAC scheme as above, eventrecording.available), or uploaded straight into an S3-compatible bucket you own as<prefix>recordings/<company>/<call>.mp3plus a.jsonsidecar. Retries with backoff; the last error is shown in Settings → Recordings.
Retention: recordings age out after the company's retention window (default 90 days, configurable; 0 keeps them) and are deleted from storage — the endpoint then returns recording_not_available. Objects are stored in a private US-hosted bucket (AWS us-east-1) once the storage cutover completes.
Coming soon
POST /v1/messages— manually capture a message- Per-key scopes on operator keys (read-only keys for reporting integrations)
- Test-mode keys (
ck_test_*) that don't mutate production data - Outbound dialing API + campaigns (
POST /v1/calls/outbound) — requires TCPA compliance review before launch
Need something not listed? Email support@getswiftlee.com — we'll prioritize based on partner demand.