← Swiftlee Answering home

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 with sl_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 with op_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."
  }
}
CodeStatusWhen
missing_api_key401no Authorization / X-API-Key header
invalid_api_key_format401not a company key (ck_live_* / sl_live_*) or operator key (ok_live_* / op_live_*)
invalid_api_key401key not recognized
api_key_revoked401key was revoked
wrong_key_kind403company key on an operator endpoint or vice versa
forbidden403company is not under your operator
not_found404no such company / row
invalid_input400validation failed (details lists each field)
conflict409duplicate or state conflict
payment_required402number quota / billing blocks a purchase
rate_limited429over 100 requests/min for the key (Retry-After set)
upstream_failed502carrier / AI vendor call failed
write_failed500database write failed

Validation failures (invalid_input) also carry a details array of { path, message }.

Endpoints

GET/v1/me

Auth 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"
}
GET/v1/calls

List calls for the authed company, newest first. Cursor-paginated.

QUERY PARAMETERS
  • limit — 1–100, default 25
  • cursor — ISO timestamp; returns calls with started_at strictly before this
  • disposition — 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"
}
GET/v1/calls/:id

Full 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.

MethodPath (under /api/v1)What it does
GET/operator/meAuth smoke test — the operator the key belongs to + client count.
GET/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.
GET/operator/companiesList the operator's clients, newest first. Cursor-paginated on created_at.
POST/operator/companiesCreate 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}/greetingSet the AI/receptionist greeting (EN and/or ES). Resyncs this company's assistant.
PUT/operator/companies/{id}/instructionsSet AI custom instructions and do-nots. Resyncs this company's assistant.
PUT/operator/companies/{id}/faqsReplace the FAQ list (max 20). Resyncs this company's assistant. GET returns the current list.
PUT/operator/companies/{id}/escalation-contactsReplace escalation contacts (max 10, optional on-call schedule). GET returns the current list.
PUT/operator/companies/{id}/business-hoursReplace the weekly schedule (send all 7 days) + optional language / after-hours mode / retention. GET returns the current values.
PUT/operator/companies/{id}/holidaysReplace the holiday calendar (dates not sent are removed) + optional overflow mode. GET returns the current calendar.
PUT/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.
PUT/operator/companies/{id}/intake-formCreate (no id) or update (id) an intake form. Resyncs this company's assistant. GET lists the forms.
PUT/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.
PUT/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).
GET/operator/companies/{id}/knowledgeList knowledge-base documents (title, source, digest, include_in_prompt).
POST/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.
GET/operator/companies/{id}/numbersThe 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}/resyncPush the company's current scripts, FAQs, knowledge, intake and transfer config to its live assistant (this company only).
GET/operator/companies/{id}/callsCalls for one client — same shape and params as the company-key GET /calls.
PUT/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).
PUT/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.
PUT/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.

Typical onboarding in five calls

  1. POST /operator/companies with an Idempotency-Key → save the returned company.id
  2. PUT …/greeting, PUT …/faqs, PUT …/escalation-contacts, PUT …/business-hours
  3. POST …/knowledge with { "url": "https://client-site.com" }
  4. POST …/numbers (buys the line; the first number also builds the assistant)
  5. POST …/resync once, then GET …/calls as calls arrive
GET/v1/operator/me

Auth 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_…"
RESPONSE 200
{
  "operator": {
    "id": "9b1d0c2e-1111-4a3b-8c9d-0e1f2a3b4c5d",
    "slug": "conversational",
    "name": "Conversational",
    "active": true
  },
  "companies": 112,
  "scopes": [],
  "api_version": "v1"
}
GET/v1/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.

PARAMETERS
  • month — YYYY-MM, default = current month
curl -X GET https://getswiftlee.com/api/v1/operator/usage \
  -H "Authorization: Bearer ok_live_…"
RESPONSE 200
{
  "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)

GET/v1/operator/companies

List the operator's clients, newest first. Cursor-paginated on created_at.

PARAMETERS
  • limit — 1–100, default 25
  • cursor — ISO created_at of the last row from the previous page
  • status — lead | trial | active | past_due | paused | cancelled
curl -X GET https://getswiftlee.com/api/v1/operator/companies \
  -H "Authorization: Bearer ok_live_…"
RESPONSE 200
{
  "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
}
POST/v1/operator/companies

Create 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.

PARAMETERS
  • 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}'
RESPONSE 201
{
  "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

GET/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_…"
RESPONSE 200
{
  "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"
  }
}
PATCH/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"}'
RESPONSE 200
{
  "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
}
PUT/v1/operator/companies/{id}/greeting

Set 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?"}'
RESPONSE 200
{
  "ok": true,
  "warning": null
}
PUT/v1/operator/companies/{id}/instructions

Set 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."}'
RESPONSE 200
{
  "ok": true,
  "warning": null
}
PUT/v1/operator/companies/{id}/faqs

Replace 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."}]}'
RESPONSE 200
{
  "faqs": [
    {
      "question": "Do you offer emergency service?",
      "answer": "Yes — 24/7, with a $150 after-hours dispatch fee."
    }
  ],
  "warning": null
}
PUT/v1/operator/companies/{id}/escalation-contacts

Replace 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"}]}'
RESPONSE 200
{
  "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"
    }
  ]
}
PUT/v1/operator/companies/{id}/business-hours

Replace 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}'
RESPONSE 200
{
  "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)

PUT/v1/operator/companies/{id}/holidays

Replace 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"}'
RESPONSE 200
{
  "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
    }
  ]
}
PUT/v1/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.

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}]}'
RESPONSE 200
{
  "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
}
PUT/v1/operator/companies/{id}/intake-form

Create (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"}}]}'
RESPONSE 200
{
  "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
}
PUT/v1/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.

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"}}'
RESPONSE 200
{
  "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
}
PUT/v1/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).

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"]}}]}'
RESPONSE 200
{
  "form_id": "…",
  "name": "Default intake",
  "steps": [
    {
      "id": "patient_name",
      "field": "Patient name",
      "ask": "What is the patient name?",
      "type": "text",
      "required": true
    }
  ],
  "warning": null
}
GET/v1/operator/companies/{id}/knowledge

List 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_…"
RESPONSE 200
{
  "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…"
    }
  ]
}
POST/v1/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.

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"}'
RESPONSE 201
{
  "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

GET/v1/operator/companies/{id}/numbers

The company's phone numbers.

curl -X GET https://getswiftlee.com/api/v1/operator/companies/$COMPANY_ID/numbers \
  -H "Authorization: Bearer ok_live_…"
RESPONSE 200
{
  "numbers": [
    {
      "id": "…",
      "e164": "+18445550123",
      "number_type": "main",
      "active": true,
      "provisioning_status": "live",
      "porting_status": "not_applicable",
      "created_at": "2026-09-29T18:04:11.000Z"
    }
  ]
}
POST/v1/operator/companies/{id}/numbersSPENDS MONEY

Search + 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"}'
RESPONSE 201
{
  "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

POST/v1/operator/companies/{id}/resync

Push 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_…"
RESPONSE 200
{
  "ok": true,
  "assistant_id": "asst_…"
}

Errors: 409 conflict — the company has no assistant (human-tier without the after-hours add-on, or not provisioned)

GET/v1/operator/companies/{id}/calls

Calls for one client — same shape and params as the company-key GET /calls.

PARAMETERS
  • limit — 1–100, default 25
  • cursor — ISO timestamp; returns calls with started_at strictly before this
  • disposition — 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_…"
RESPONSE 200
{
  "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"
}
PUT/v1/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).

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}]}'
RESPONSE 200
{
  "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
}
PUT/v1/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.

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}]}'
RESPONSE 200
{
  "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

PUT/v1/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.

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}'
RESPONSE 200
{
  "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}}."
  }]
}
  1. The caller says “where’s my order 4521?” → the AI calls lookup_order(order_number: "4521").
  2. Our proxy renders GET https://api.client.com/orders/4521 with Authorization: Bearer sk_live_…, times out after 6s, follows no redirects.
  3. The reply { "data": { "order": { "id": "4521", "status": "packed", "eta": "Tuesday" } } } is narrowed by response_path, then response_template becomes what the AI is told: “Order 4521 is packed and ships Tuesday.”
  4. 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, passport and up to 10 custom patterns. 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.captured payloads, 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. With card on, 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) with begin_sensitive_capture / end_sensitive_capture, which pause and resume the carrier recording; markers land in the call’s recording_paused_segments and 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.

EventFires when
message.capturedA message was taken for the client (post-call workflow).
call.completedEvery finished call, any disposition.
call.qualifiedAnswered call with urgency urgent/emergency.
call.disqualifiedAnswered call with routine urgency.
call.voicemailCaller left a voicemail.
call.no_answerMissed call (partner-integration name).
call.missedLegacy alias of call.no_answer; fires at the same point.
callback.queuedA callback was queued for a receptionist.
appointment.bookedThe assistant booked an appointment (payload adds booking_ref, provider, service, location; notes withheld under hipaa_mode).
appointment.cancelledThe caller cancelled a booking by phone (W8) — booking_ref, starts_at, provider, reason (withheld under hipaa_mode).
appointment.rescheduledThe caller moved a booking by phone (W8) — booking_ref, previous_starts_at, starts_at, ends_at, provider.
call.escalatedA 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=json for { 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.completed and message.captured then also carry recording_url (signed, 1 hour), recording_expires_at and transcript. 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, event recording.available), or uploaded straight into an S3-compatible bucket you own as <prefix>recordings/<company>/<call>.mp3 plus a .json sidecar. 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.