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 (claude.ai or the desktop app). Settings → Connectors → Add custom connector, paste the address, then sign in to SIPmind and press Allow. You choose Full or Read only; the connection appears in Account → API keys, where it can be switched off.
- Claude Code. With a key from Account → API keys:
claude mcp add --transport http sipmind https://sipmind.net/mcp \ --header "Authorization: Bearer sk-tenant-YOUR_KEY"
- Cursor — .cursor/mcp.json:
{
"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.
| Event | When | data carries |
|---|---|---|
| lead.created | A 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_changed | A lead moves on its pipeline. | lead_id, stage, close_reason |
| lead.merged | Two leads of one person become one. | lead_id (kept), merged_lead_id (gone), moved |
| lead.deleted | A lead is deleted in the dashboard. Delete your copy. | lead_id — nothing else: its data is gone |
| contact.created | A new person is in the contact book. | contact_id, full_name, phones, emails, channel |
| contact.updated | A contact gets a new name, phone, email, fields or owner. | contact_id, fields (what changed), full_name, phones, emails |
| contact.merged | Two contacts of one person become one. | contact_id (kept), merged_contact_id (gone), moved |
| contact.deleted | A person is erased («forget this person»). Delete your copy. | contact_id — nothing else: the personal data is gone |
| call.ended | A 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.created | A 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.cancelled | A visit is cancelled, by anyone. | appointment_id, practitioner_id, contact_id, starts_at_utc, ends_at_utc, service |
| task.created | A 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.completed | A 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
| Endpoint | What it does |
|---|---|
| The agent | |
| GET /api/v1/agent/config | Current behaviour, the allowed values for every field, and the plan's limits. |
| PATCH /api/v1/agent/config | Business 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/test | Ask 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/capabilities | What 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/tools | The 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/people | The humans on the account — the ids assigned_to and owner_id point at, and who may be handed work. |
| GET/PATCH /api/v1/team/routing | Who 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/employees | AI 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}/lines | Give an employee its own line or channel, or take it away. |
| Channels | |
| GET /api/v1/channels | Setup checklist: connected, half-connected or absent, per channel. |
| PATCH /api/v1/channels/whatsapp-qr | A 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/telegram | Verify the bot token, store it, register the webhook — or disconnect. |
| POST /api/v1/channels/whatsapp | Route 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/email | Connect 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/twilio | Connect 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/phone | Route 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/settings | The Instagram comment→DM funnel: switches, working hours and days, the default scenario. |
| GET /api/v1/channels/instagram/scenarios | Codeword scenarios. PUT or DELETE /channels/instagram/scenarios/{id} to save or remove one. |
| GET /api/v1/channels/instagram/funnel | The funnel's numbers per scenario, for the last N days. |
| Knowledge | |
| GET/POST /api/v1/documents | List the knowledge base, or add to it. /documents/stats for counts, GET or DELETE /documents/{id} for one. |
| POST /api/v1/query | Search that knowledge base the way the agent does. |
| CRM | |
| GET/POST /api/v1/leads | Leads, 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/unsorted | Requests 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-fields | The account's own lead fields: key, label, type, and the allowed values of a select. |
| POST /api/v1/leads/{id}/assign | Give 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/deals | Move 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/pipelines | The account's pipelines and each one's stages, with what every stage means: open, won or lost. |
| PATCH /api/v1/leads/{id}/stage | Move a lead to a stage of its own pipeline, mirroring a change in your CRM. |
| POST /api/v1/leads/{id}/answered | Close 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/automations | The 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}/runs | What a rule did lately, per card: task_created, sent, window_closed, left_stage … |
| GET/POST /api/v1/tasks | CRM 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}/complete | Done, 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/practitioners | Who 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-slots | The next free start times — the same windows the agent offers callers. |
| GET/POST /api/v1/appointments | Bookings, 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/calls | Every interaction — phone, WhatsApp, Telegram, Instagram, widget. |
| GET /api/v1/calls/{id} | One interaction with its transcript and AI summary. |
| GET/POST /api/v1/contacts | The 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/duplicates | Cards 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/companies | Companies 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/notes | Write 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}/comments | The lead's comment thread. An @name in a comment tells that colleague, as on the card. |
| Outbound calls | |
| GET/PATCH /api/v1/outbound/settings | Caller 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/call | Place one call. POST /outbound/call/{id}/cancel stops a queued one; GET /outbound/calls lists them. |
| GET/POST /api/v1/outbound/dnc | The do-not-call list. DELETE /outbound/dnc/{id} takes a number off it. |
| Events | |
| GET/PUT /api/v1/events/subscription | Where 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:
- Creating an account. Clients register themselves; you work with their key.
- Connecting Instagram, and a WhatsApp number that is not yet on a Meta app you manage — those run through the dashboard's one-click flow, because the consent happens on Meta's side and needs a browser.
- Connecting Google Calendar and switching booking reminders on — those are set in the dashboard.
- Taking a phone or an e-mail off a contact. New ones are added by POST /api/v1/contacts.
- Deleting contacts, companies, notes, calls or conversations. Ask us and we will do it — a partner key should not be able to erase a client's history.
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.