For developers

Set a client up from your own code

Describe the business, connect WhatsApp, Telegram or a phone number, load the knowledge base, and sync the leads and conversations back into your own system. One bearer token, plain REST, a published OpenAPI schema.

Quickstart OpenAPI 3.1 schema

Authentication

Every request carries the client's API key as a bearer token. The client creates it in their dashboard under Account → API keys; it is shown once. The key identifies the tenant, so no endpoint takes a tenant id — you cannot reach another account's data by construction, not by a check somebody remembered to write.

curl https://sipmind.net/api/v1/agent/config \
  -H "Authorization: Bearer sk-tenant-YOUR_KEY"

Two kinds of key. A Full key does everything below. A Widget-only key is public by design — it lives in a web page's source — and is accepted only on the widget endpoints; anywhere else it returns 403. Never put a Full key in a browser.

When a key is refused. 401 — no key, or one we do not know. 403 API key is deactivated — that one key was revoked; the client creates another. 403 This account is switched off. — the whole account is off: every key is refused, on every endpoint and on /mcp, until it is switched back on. Its agent stops answering calls and messages too, the incoming-lead webhook answers 403 and no lead webhooks are sent. Nothing is deleted, and the same key works again once it is on. The client writes to hello@sipmind.net. 402 — the trial ended; the message links to the page that takes payment.

Quickstart: zero to answering

Four calls. The client registers themselves and gives you a key; everything after that is yours.

01

Tell the agent what the business is

You supply the facts; the behaviour — how to greet, when to hand over to a human, how to speak on a phone line — is ours, kept up to date, and is neither editable nor shown. GET /api/v1/agent/config names the field that holds your text in prompt_field: business_context on every current account.

curl -X PATCH https://sipmind.net/api/v1/agent/config \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "business_context": "Dental clinic in Almaty. Mon-Sat 9-19. Cleaning 15000 KZT, implant from 250000 KZT. Address: Abay 15.",
    "welcome_message": "Hi, you have reached Brightwater Dental. How can I help?",
    "timezone": "Asia/Almaty"
  }'

Send only what you are changing — omitted fields keep their value.

02

Load what it should know

Price lists, FAQs, procedure descriptions. The agent answers from these.

curl -X POST https://sipmind.net/api/v1/documents \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"content": "Teeth whitening — $350, takes about 60 minutes…"}'

03

Connect a channel

One call does the whole handshake: we verify the token with Telegram, store it and register the webhook. If the webhook does not register you get an error — never a green answer with a channel that silently receives nothing.

curl -X POST https://sipmind.net/api/v1/channels/telegram \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"bot_token": "123456:AA..."}'

# WhatsApp, when the number already sits on a Meta app you manage.
# phone_number_id is the numeric id from WhatsApp Manager — not the phone number.
curl -X POST https://sipmind.net/api/v1/channels/whatsapp \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"phone_number_id": "1236893399497040", "access_token": "EAA..."}'

# check every channel at once, including the half-connected ones
curl https://sipmind.net/api/v1/channels -H "Authorization: Bearer $KEY"

half_connected: true means credentials are stored but the provider handshake is missing — the state that looks connected on every screen and delivers nothing. Instagram, and WhatsApp numbers that do not yet exist on any Meta app, still need Meta's browser flow: send the client to their dashboard once, then keep working from code.

04

Pull the results into your system

curl "https://sipmind.net/api/v1/leads?updated_since=2026-08-01T00:00:00Z" \
  -H "Authorization: Bearer $KEY"

# → {"leads": [...], "count": 12,
#    "next_updated_since": "2026-08-12T09:14:02.881Z",
#    "next_after_id": "6f1c…", "has_more": false}

Keeping in sync

Lists are ordered by updated_at ascending and cursored on it. Store the next_updated_since and next_after_id you get back and pass both in next time, as updated_since and after_id; keep going while has_more is true.

Pass both. An import or a bulk edit changes hundreds of rows in the same instant; with the timestamp alone a group like that larger than your page comes back as the same page again. With after_id the next page starts exactly after the last row you got.

Ascending is not a style choice. Newest-first with an offset loses rows: while you page through, new rows land at the top and push the boundary, so page two repeats some and skips others. With an ascending cursor a changed row moves to the end — which is where your next poll meets it.

At-least-once, on purpose. Rows sharing the cursor's exact timestamp can come back on the next poll, so upsert by id. The alternative would drop rows, and a sync that quietly loses data is worse than one that repeats it.

Let your AI assistant do it

SIPmind speaks MCP, so Claude, Cursor, Codex or any MCP client can set a client up and read their data in plain language: “describe this clinic to the agent from their website, connect the Telegram bot, load the price list, ask the agent what a cleaning costs, and show me last week's leads.” The tools cover configuring the agent — down to the facts it must say word for word, what it is allowed to do and its own tools — trying it the way a customer would, hiring AI employees and giving each one its own line, outbound calling, channels, the knowledge base, the Instagram comment→DM funnel, who a conversation is handed to and when they are in, webhooks, and the CRM: leads with their deal value, own fields, owner and tags, contacts, duplicates, tasks, pipelines, conversations and companies.

Connect by address — nothing to install

The server lives at https://sipmind.net/mcp.

claude mcp add --transport http sipmind https://sipmind.net/mcp \
  --header "Authorization: Bearer sk-tenant-YOUR_KEY"
{
  "mcpServers": {
    "sipmind": {
      "url": "https://sipmind.net/mcp",
      "headers": { "Authorization": "Bearer sk-tenant-YOUR_KEY" }
    }
  }
}

Or run it on your machine

One file, no dependencies, any Python 3.10+ — the same tools as the address above:

curl -O https://sipmind.net/mcp/sipmind_mcp.py

Claude Code — .mcp.json in that folder:

{
  "mcpServers": {
    "sipmind": {
      "command": "python3",
      "args": ["./sipmind_mcp.py"],
      "env": { "SIPMIND_API_KEY": "sk-tenant-YOUR_KEY" }
    }
  }
}

Claude Desktop — the same entry in claude_desktop_config.json, with the full path to the file in args. Codex — ~/.codex/config.toml:

[mcp_servers.sipmind]
command = "python3"
args = ["/full/path/to/sipmind_mcp.py"]
env = { SIPMIND_API_KEY = "sk-tenant-YOUR_KEY" }

If the first call answers that your Python cannot verify certificates, that is your machine's trust store, not us: run Install Certificates.command (macOS python.org builds) or pip install certifi.

Which key

A Full key sets things up. A Read only key — Account → API keys → Access — pulls leads, conversations and reports and changes nothing: the right one for an export, or for an assistant that should only look.

Try the agent before a customer does

ask_agent (REST: POST /api/v1/agent/test) sends what a customer would write and returns the agent's reply, built exactly as a live chat builds it — same prompt, knowledge base and tools. Nothing is sent, saved or booked: the tools the agent chose come back in would_call — for example submit_lead with the name and phone it would have saved — and are not run; the reply is then what the agent writes once they succeed, and tools_simulated says the success was a stand-in. Pass employee_id to try an AI employee while it is still a draft. It spends model tokens, so it takes a Full key.

curl -X POST https://sipmind.net/api/v1/agent/test \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"message": "How much is a cleaning? I am Anna, +77001234567", "channel": "whatsapp"}'
# → {"reply": "…", "would_call": [{"tool": "submit_lead", "arguments": {"name": "Anna", …}}], …}

Two things a browser has to do once. Connecting an Instagram account is a consent Meta requires a human to grant on screen, and a WhatsApp Business Account is shared with us the same way — so those are one click in the dashboard, not an API call. Everything that happens after the connection is code.

Deliberately no dangerous verbs. The MCP surface can set up, read and try; it cannot place a call, send a paid WhatsApp template or delete anything. An agent following a half-understood instruction should not be able to spend your client's money — those actions stay in the API, where a human writes the request on purpose.

Events pushed to you

Polling is the reliable path; events are the fast one. Subscribe and we POST to your endpoint when something happens. A new account receives lead.created only, so name the events you want.

EventWhendata carries
lead.createdA lead is captured — by the agent, a form, the API or a person.lead_id, contact_id, name, phone, email, source, stage, message, tags
lead.stage_changedA lead moves on its pipeline.lead_id, stage, close_reason
lead.mergedTwo leads of one person become one.lead_id (kept), merged_lead_id (gone), moved
lead.deletedA lead is deleted in the dashboard. Delete your copy.lead_id — nothing else: its data is gone
contact.createdA new person is in the contact book.contact_id, full_name, phones, emails, channel
contact.updatedA contact gets a new name, phone, email, fields or owner.contact_id, fields (what changed), full_name, phones, emails
contact.mergedTwo contacts of one person become one.contact_id (kept), merged_contact_id (gone), moved
contact.deletedA person is erased («forget this person»). Delete your copy.contact_id — nothing else: the personal data is gone
call.endedA phone or WhatsApp call the agent took ends.call_id, channel, direction, from, to, contact_id, duration_seconds, ended_at — the transcript and summary come from GET /calls/{id}
appointment.createdA visit is booked — by the agent or in the calendar.appointment_id, practitioner_id, practitioner_name, contact_id, starts_at_utc, ends_at_utc, service, source
appointment.cancelledA visit is cancelled, by anyone.appointment_id, practitioner_id, contact_id, starts_at_utc, ends_at_utc, service
task.createdA task is set — by a person or the agent.task_id, title, status, due_at, contact_id, lead_id, company_id, assigned_to, source
task.completedA task is done.the same, with completed_at and completed_by
PUT /api/v1/events/subscription
{
  "url": "https://your-endpoint.example/hook",
  "events": ["lead.created", "lead.stage_changed", "call.ended"],
  "signing_secret": "<16+ characters you choose>"
}

The secret is write-only: we report whether one is set, never its value. Then every delivery looks like this.

POST https://your-endpoint.example/hook
X-SIPmind-Event: lead.created
X-SIPmind-Signature: sha256=<hmac of the raw body with your secret>

{
  "event": "lead.created",
  "tenant_id": "0de00000-…",
  "occurred_at": "2026-08-13T04:12:07.881Z",
  "data": {
    "lead_id": "9f2c…", "contact_id": "3a71…",
    "name": "Anna", "phone": "+77001234567", "email": "",
    "source": "whatsapp", "stage": "new", "message": "…"
  }
}

Verify the signature against the raw body before trusting it. We retry a 5xx or a timeout three times with growing gaps; a 4xx is treated as your endpoint saying "don't", and we stop.

Events complement the sync, they do not replace it. A delivery can still be lost — your endpoint is down for an hour, a retry budget runs out. Treat events as a nudge and let the cursored updated_since poll be the source of truth. That combination is what makes a sync you never have to reconcile by hand.

Endpoints

EndpointWhat it does
The agent
GET /api/v1/agent/configCurrent behaviour, the allowed values for every field, and the plan's limits.
PATCH /api/v1/agent/configBusiness facts, greeting, voice and speaking tempo, facts said word for word, callers never answered, knowledge-base tuning, timezone, notification language, and the languages your customers speak.
POST /api/v1/agent/testAsk the agent: its reply to what a customer would write, and the tools it would call — nothing is sent, saved or booked.
GET/PATCH /api/v1/agent/capabilitiesWhat the agent is allowed to do: booking, reading photos, remembering customers, follow-ups, hand-over tasks, approval of its replies — every switch with its value, default and one line on what it does. Send only the ones you flip.
GET/PUT /api/v1/agent/toolsThe agent's own tools: HTTP webhooks into the client's systems it may call mid-conversation (check an order, look up a booking), with the JSON Schema of the arguments it gathers first. The Authorization header goes in and never comes back.
GET /api/v1/team/peopleThe humans on the account — the ids assigned_to and owner_id point at, and who may be handed work.
GET/PATCH /api/v1/team/routingWho a conversation is handed to — phones, PBX extensions, queues, named per department or colleague — the hours and days off when someone is in, and the Telegram group that hears about new leads.
GET/POST /api/v1/employeesAI employees: list them, or hire one. A new one is a draft and answers nobody until it is switched on.
PATCH /api/v1/employees/{id}Change an employee; status: active puts it on duty.
PUT/DELETE /api/v1/employees/{id}/linesGive an employee its own line or channel, or take it away.
Channels
GET /api/v1/channelsSetup checklist: connected, half-connected or absent, per channel.
PATCH /api/v1/channels/whatsapp-qrA WhatsApp number linked by QR: when the assistant answers (never / at once / after N minutes nobody answered) and the people it keeps out of. Its state — linked, reconnecting, restricted, unlinked — is channels.whatsapp_qr.state in the checklist. Linking is done with the phone, in the dashboard.
POST/DELETE /api/v1/channels/telegramVerify the bot token, store it, register the webhook — or disconnect.
POST /api/v1/channels/whatsappRoute a WhatsApp number you already manage. The token is checked with Meta before anything is stored; a number connected elsewhere is refused. DELETE /channels/whatsapp/{phone_number_id} unroutes it.
POST/PATCH/DELETE /api/v1/channels/emailConnect the business's own mailbox with an app password (Gmail, Google Workspace, Yandex, Mail.ru or any IMAP host; not Microsoft 365 / Outlook.com yet). We sign in to IMAP and SMTP before anything is stored, keep the password encrypted and never return it, and never answer mail that was already in the inbox. PATCH switches ai_answers; DELETE disconnects.
POST/DELETE /api/v1/channels/twilioConnect the client's own Twilio account with its Account SID and Auth Token. We check the pair with Twilio before anything is stored, keep the token encrypted and never return it. Send numbers to route them: each is proven to be on that account and pointed at SIPmind on Twilio's side. Calls in, calls out and transfers are billed by Twilio to that account. DELETE disconnects.
GET/POST /api/v1/channels/phoneRoute an existing number (Telnyx, your carrier) to the agent; a number on your own Twilio account goes through /channels/twilio. DELETE /channels/phone/{id} removes one.
GET/PATCH /api/v1/channels/instagram/settingsThe Instagram comment→DM funnel: switches, working hours and days, the default scenario.
GET /api/v1/channels/instagram/scenariosCodeword scenarios. PUT or DELETE /channels/instagram/scenarios/{id} to save or remove one.
GET /api/v1/channels/instagram/funnelThe funnel's numbers per scenario, for the last N days.
Knowledge
GET/POST /api/v1/documentsList the knowledge base, or add to it. /documents/stats for counts, GET or DELETE /documents/{id} for one.
POST /api/v1/querySearch that knowledge base the way the agent does.
CRM
GET/POST /api/v1/leadsLeads, cursored for incremental sync — or push one in from your own system. Each lead names the pipeline it is on, the person on your team who owns it (assigned_to) and its tags.
GET/PATCH /api/v1/leads/{id}One lead with every field — and change it: who it is, the deal value, currency, expected close date and probability, the account's own fields (merged; a key sent as null is cleared), the company, the pipeline. Send external_ref when you create a lead and a retry returns the lead already made instead of a second one.
GET /api/v1/leads/unsortedRequests waiting in «Unsorted» for a person to sort them. POST /api/v1/leads/{id}/accept puts one on its board (optionally for a named person); POST /api/v1/leads/{id}/reject takes it off as noise. A request is decided once: a second decision answers 409.
GET /api/v1/leads/custom-fieldsThe account's own lead fields: key, label, type, and the allowed values of a select.
POST /api/v1/leads/{id}/assignGive the lead to a person from /team/people, or to nobody. POST /api/v1/leads/{id}/tags puts tags on and takes them off; POST /api/v1/leads/{id}/merge folds it into another open request of the same person.
POST /api/v1/imports/dealsMove a client's deals over from another CRM — up to 500 per call, quietly (no event per card, no auto-owner, no automation), each person found or created once. A deal sent again is skipped by its external_ref; a bad batch is refused whole with every problem named. GET /api/v1/imports/{id} gives the counts; the import can be undone in the dashboard.
GET /api/v1/pipelinesThe account's pipelines and each one's stages, with what every stage means: open, won or lost.
PATCH /api/v1/leads/{id}/stageMove a lead to a stage of its own pipeline, mirroring a change in your CRM.
POST /api/v1/leads/{id}/answeredClose a complaint or question the customer got an answer to in your own system. Pipelines that work this way say closes_when_answered; each lead carries answered_at and answered_via.
GET/POST /api/v1/automationsThe account's own rules: when a card enters a stage or a new card appears, create a task, give it to somebody, move it, tag it, write to the customer or tell the managers — right away or after a delay. The listing also says what each part may hold. A rule acts only on what happens after it was switched on.
GET/PATCH/DELETE /api/v1/automations/{id}One rule. Send only what changes; enabled alone switches it.
GET /api/v1/automations/{id}/runsWhat a rule did lately, per card: task_created, sent, window_closed, left_stage …
GET/POST /api/v1/tasksCRM tasks, cursored for sync (status narrows to pending, completed or dismissed) — or create one for a person; without assigned_to the account's own rules choose the owner.
GET/PATCH /api/v1/tasks/{id}One task; change its title, notes, due date, priority or repetition.
POST /api/v1/tasks/{id}/completeDone, with result — required where the account closes tasks only with one. POST /api/v1/tasks/{id}/dismiss closes one as not needed; POST /api/v1/tasks/{id}/assign hands it to a person from /team/people, who is told on their phone and Telegram.
Booking
GET/POST /api/v1/practitionersWho can be booked — people, rooms, equipment — with weekly hours in local time. PATCH /api/v1/practitioners/{id} changes one; is_active: false switches it off. A schedule with a mistake is refused by name, never saved half-read.
GET /api/v1/practitioners/{id}/free-slotsThe next free start times — the same windows the agent offers callers.
GET/POST /api/v1/appointmentsBookings, cursored for sync — or book one. A booking is checked as the agent checks it: not in the past, inside working hours, not taken, free in the connected Google Calendar; a refusal is a 409 naming why. "check": false books like a person in the dashboard, for moving history in.
GET/PATCH /api/v1/appointments/{id}One booking; PATCH moves it to another time — reminders are sent again for the new time and the Google event moves with it. POST /api/v1/appointments/{id}/status confirms, completes, cancels, marks a no-show or brings a cancelled one back.
GET /api/v1/callsEvery interaction — phone, WhatsApp, Telegram, Instagram, widget.
GET /api/v1/calls/{id}One interaction with its transcript and AI summary.
GET/POST /api/v1/contactsThe client's contact book, with the person responsible for each contact (assigned_to), its tags and free-form facts — or find-or-create a person: a phone or e-mail the account already knows returns that person, with anything new merged in.
GET/PATCH /api/v1/contacts/{id}One person; change the name, the facts (merged) or the company. POST /api/v1/contacts/{id}/assign names who is responsible, POST /api/v1/contacts/{id}/tags tags them.
GET /api/v1/contacts/duplicatesCards that share a phone or an e-mail, older card first. POST /api/v1/contacts/{id}/merge with {"into": …} folds one into the other — calls, chats, requests and tasks move with it. Merging is not in MCP: it cannot be undone.
GET/POST /api/v1/companiesCompanies those contacts belong to — or create one. A registration number or a name the account already has returns that company instead of a second card.
GET/PATCH /api/v1/companies/{id}One company; change its name, registration number, status, plan, owner, own fields (merged) or notes. GET /api/v1/companies/{id}/people lists who speaks for it; a person joins by PATCH /api/v1/contacts/{id} with its company_id.
POST /api/v1/notesWrite down a touch that happened outside the platform — a meeting, a call from a personal phone, an email — on a person, a company or both. GET /api/v1/contacts/{id}/notes and GET /api/v1/companies/{id}/notes read them back.
GET/POST /api/v1/leads/{id}/commentsThe lead's comment thread. An @name in a comment tells that colleague, as on the card.
Outbound calls
GET/PATCH /api/v1/outbound/settingsCaller ID, what the agent calls about, its first line, and the hours it may dial — with ready and the blockers that stop a call.
POST /api/v1/outbound/callPlace one call. POST /outbound/call/{id}/cancel stops a queued one; GET /outbound/calls lists them.
GET/POST /api/v1/outbound/dncThe do-not-call list. DELETE /outbound/dnc/{id} takes a number off it.
Events
GET/PUT /api/v1/events/subscriptionWhere we POST events, which ones, and the secret they are signed with.

Full request and response shapes: the OpenAPI schema — point your generator at it.

Things worth knowing before you build

Secrets go in, never out

Channel tokens, an AI key you bring and passwords can be set through the API and are never returned by it. Your setup script therefore cannot leak a client's channel token — and cannot accidentally erase one either.

Transcripts are personal data

Most of our tenants are clinics. Lists never carry transcripts; you fetch them one at a time from /calls/{id}, so pulling them is a deliberate act rather than a side effect of a sync.

Rate limit and request ids

60 requests per minute per key. Responses carry X-RateLimit-Remaining, and Retry-After when throttled. Every response has an X-Request-ID — quote it when you write to us and we can find the exact request.

Plans gate capability, and say so

A text-only plan refuses a phone number with 402 rather than accepting it and never ringing. The voice engine is chosen by us unless the account runs on its own AI key: then model_locked is false and the choices are listed. Otherwise a write of it is not applied, and the response says so in a notice.

What the API does not do yet

So you can plan around it rather than discover it:

Need one of these? Write to hello@sipmind.net — this list is ordered by what people ask for.

Build against a real account

A free trial gives you a working tenant and a key in a couple of minutes.