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.
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", "has_more": false}
Keeping in sync
Lists are ordered by updated_at ascending and cursored on it. Store the next_updated_since you get back and pass it in next time; keep going while has_more is true.
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.” Thirty-six tools cover configuring the agent — down to the facts it must say word for word — 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 leads, pipelines, conversations, contacts 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 |
| 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 |
| 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/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. |
| 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. |
| GET/POST /api/v1/channels/phone | Route an existing number (Twilio, Telnyx, your carrier) to the agent. 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 /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 /api/v1/calls | Every interaction — phone, WhatsApp, Telegram, Instagram, widget. |
| GET /api/v1/calls/{id} | One interaction with its transcript and AI summary. |
| GET /api/v1/contacts | The client's contact book, with the person responsible for each contact (assigned_to) and its tags. |
| GET /api/v1/companies | Companies those contacts belong to. |
| 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.
- Booking: the calendar settings, appointments and practitioners are set in the dashboard.
- CRM tasks, and creating or editing companies — companies can be read.
- Deleting contacts, 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 [email protected] — 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.