# VanceTel — complete developer documentation > The programmable phone system you build your workflows around. A real softphone (iOS, iPad, Mac, Apple Vision Pro, desktop and web) with a production REST API, webhooks, deep links and an MCP server behind it — the developer-friendly middle ground between a CPaaS and a closed softphone app. VanceTel lets developers send programmable SMS and MMS, place click-to-call deep links, sync contacts, turn website chat into two-way SMS, and react to HMAC-signed webhooks — then embed calling and texting into their own software, CRM and dashboards. The public API is write/action and webhook oriented. The primary call to action is "Book a demo". This file is the entire documentation set concatenated as Markdown, for indexing and retrieval. Each page below is preceded by its canonical URL — cite that URL, not this file. Accuracy notes for citation: deep links pre-fill the recipient and, for the sms action, an optional draft message body that opens editable in the composer and is never auto-sent (programmatic sending uses the REST API); webhooks are HMAC-SHA256 signed; the softphone runs on iOS, iPad, Mac, Apple Vision Pro, Windows/Mac desktop and the web. Pricing is indicative and confirmed during a demo. ## Contents - [Introduction](https://vancetel.com/developers/docs): Developer documentation for the programmable softphone, SMS and voice API — send messages, place calls, sync contacts, and subscribe to webhooks. - [Quickstart](https://vancetel.com/developers/docs/quickstart): Send your first SMS with the API in a few minutes — get a key, make one request, and watch it arrive in the softphone. - [Authentication](https://vancetel.com/developers/docs/authentication): Authenticate API requests with a bearer API key, keep it server-side, and rotate keys safely. - [API reference](https://vancetel.com/developers/docs/api-reference): Interactive VanceTel API reference — every endpoint with a live "try it" console. - [Send an SMS](https://vancetel.com/developers/docs/sms): Send an SMS or MMS from a business number with one POST, read the message object, and track delivery with signed delivery-receipt webhooks. - [Group SMS](https://vancetel.com/developers/docs/group-sms): Open a multi-participant SMS conversation, thread every reply into one stable conversation, and add or remove participants over the API. - [Opt-out & compliance](https://vancetel.com/developers/docs/opt-out): Automatic STOP/START/HELP handling, blocked-recipient rejection before send, and TCPA and A2P 10DLC context for compliant business SMS. - [Deep links](https://vancetel.com/developers/docs/deep-links): Use deep links to launch a call or a pre-addressed text in the softphone from any button, record or dashboard — the recipient is pre-filled for one-tap dialing. - [Contacts](https://vancetel.com/developers/docs/contacts): Create and retrieve contacts with the API — sync people into the shared softphone directory so inbound calls and texts resolve to a known name. - [Webhooks](https://vancetel.com/developers/docs/webhooks): Subscribe to webhooks for inbound messages, delivery receipts and call events — verify the HMAC-SHA256 signature, return 2xx fast, and deduplicate by event id. - [Errors](https://vancetel.com/developers/docs/errors): Understand API errors — the error object shape, HTTP status codes, example payloads, and how to handle failures from the messaging and contacts endpoints. - [Rate limits](https://vancetel.com/developers/docs/rate-limits): How API rate limits work — per-key limits, the 429 response and Retry-After header, exponential backoff, and a retry-on-429 loop in Node and Python. - [MCP server](https://vancetel.com/developers/docs/mcp): Let AI agents and Claude Code act on your phone system — send a text, create a contact, check message status — via the VanceTel MCP server. - [API changelog](https://vancetel.com/developers/docs/changelog): Reverse-chronological changelog for the VanceTel API — new endpoints, webhooks, and breaking-change notices. --- Source: https://vancetel.com/developers/docs Markdown: https://vancetel.com/developers/docs.md # VanceTel API VanceTel is a programmable phone system: a real softphone app with a production REST API, webhooks, deep links and an MCP server behind it. Use the API to send SMS and MMS, place click-to-call deep links, sync contacts, and react to inbound messages and call events — everything your team sees in the softphone is also reachable from your own software. The public API is **write- and event-oriented**: you create and send (messages, contacts, deep links) and you subscribe to webhooks for inbound activity. The softphone app surfaces the full conversation and call history in real time. ## Base URL All API requests go to a single versioned base URL over HTTPS: ```bash https://api.netexem.com/v1 ``` ## What you can build - **Programmable SMS & MMS** — two-way messaging from dedicated business numbers, with delivery receipts and automatic opt-out handling. See [Send an SMS](/developers/docs/sms). - **Click-to-call & deep links** — launch a call or a pre-addressed text from any button or record. See [Deep links](/developers/docs/deep-links). - **Contacts** — create and sync contacts on the fly; resolve caller ID. See [Contacts](/developers/docs/contacts). - **Webhooks** — subscribe to inbound messages, delivery receipts and call events, each HMAC-signed. See [Webhooks](/developers/docs/webhooks). - **MCP server** — let agents and Claude Code act on the phone system in the loop. See [MCP server](/developers/docs/mcp). ## Next steps 1. [Quickstart](/developers/docs/quickstart) — send your first text in a few minutes. 2. [Authentication](/developers/docs/authentication) — get and use an API key. 3. [API reference](/developers/docs/api-reference) — every endpoint, with a built-in console to try it live. --- Source: https://vancetel.com/developers/docs/quickstart Markdown: https://vancetel.com/developers/docs/quickstart.md # Quickstart Send your first text message in three steps. Everything here works against the free Developer sandbox. ## 1. Get an API key Create a key from your dashboard (or ask for sandbox access during your demo). Treat it like a password — it carries full account access. See [Authentication](/developers/docs/authentication) for details. ```bash export NETEXEM_API_KEY="sk_test_…" ``` ## 2. Send a message One `POST` to `/v1/messages` sends an SMS from one of your numbers. The same message also appears in the softphone inbox in real time. **curl** ```bash curl https://api.netexem.com/v1/messages \ -H "Authorization: Bearer $NETEXEM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+15551234567", "to": "+15557654321", "body": "Hello from the API" }' ``` **Node** ```ts const res = await fetch("https://api.netexem.com/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.NETEXEM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: "+15551234567", to: "+15557654321", body: "Hello from the API", }), }); const message = await res.json(); ``` **Python** ```python import os, requests res = requests.post( "https://api.netexem.com/v1/messages", headers={"Authorization": f"Bearer {os.environ['NETEXEM_API_KEY']}"}, json={ "from": "+15551234567", "to": "+15557654321", "body": "Hello from the API", }, ) message = res.json() ``` Phone numbers use [E.164 format](/developers/glossary/e164) — a leading `+` and country code, e.g. `+15551234567`. ## 3. Receive the reply When the recipient texts back, we send a signed `message.received` webhook to your endpoint and the conversation appears in the softphone. Set up your endpoint in [Webhooks](/developers/docs/webhooks). ## Next steps - [Authentication](/developers/docs/authentication) — keys, scopes and rotation - [Send an SMS](/developers/docs/sms) — the full message object, MMS and delivery receipts - [API reference](/developers/docs/api-reference) — try every endpoint live --- Source: https://vancetel.com/developers/docs/authentication Markdown: https://vancetel.com/developers/docs/authentication.md # Authentication The API authenticates every request with a bearer **API key** sent in the `Authorization` header. ```bash Authorization: Bearer sk_live_… ``` ## Keys - **Test keys** (`sk_test_…`) run against the sandbox with test numbers — no real messages, no charges. - **Live keys** (`sk_live_…`) send real traffic and bill carrier pass-through. An API key carries full account access. **Never** ship it in client-side code, a mobile app, or a public repo. Keep it on your server and inject it from an environment variable or secret manager. ## Making an authenticated request ```bash curl https://api.netexem.com/v1/contacts \ -H "Authorization: Bearer $NETEXEM_API_KEY" ``` A missing or invalid key returns `401 Unauthorized`. See [Errors](/developers/docs/errors) for the full error shape. ## Rotating keys Create a new key before retiring the old one so there is no downtime, deploy the new value, then revoke the previous key. Revocation takes effect immediately. ## Webhook authenticity API keys authenticate requests **you** make. To verify requests **we** send to your webhook endpoint, validate the HMAC-SHA256 signature — see [Webhooks](/developers/docs/webhooks). --- Source: https://vancetel.com/developers/docs/sms Markdown: https://vancetel.com/developers/docs/sms.md # Send an SMS A single `POST /messages` sends an SMS from one of your numbers to any recipient. The call returns a [message object](#the-message-object) you can store and reconcile against later delivery events. Every message you send also lands in the [softphone inbox](#delivery-status) so your team sees the same conversation in real time. The messaging API is **write- and event-oriented**: you send with `POST /messages`, then react to inbound texts and delivery updates over [Webhooks](/developers/docs/webhooks). To look up a single message after the fact, use `GET /messages/{id}`. ## Send a message Provide a `from` (an [E.164](/developers/glossary/e164) number you own), a `to` recipient, and a `body`. The API responds `201` with the created message in `queued` status. **curl** ```bash curl https://api.netexem.com/v1/messages \ -H "Authorization: Bearer $NETEXEM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+15551234567", "to": "+15557654321", "body": "Your appointment is confirmed for 2pm — reply STOP to opt out.", "senderName": "Appointment Reminder Bot" }' ``` **Node** ```ts const res = await fetch("https://api.netexem.com/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.NETEXEM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: "+15551234567", to: "+15557654321", body: "Your appointment is confirmed for 2pm — reply STOP to opt out.", senderName: "Appointment Reminder Bot", }), }); const message = await res.json(); ``` **Python** ```python import os, requests res = requests.post( "https://api.netexem.com/v1/messages", headers={"Authorization": f"Bearer {os.environ['NETEXEM_API_KEY']}"}, json={ "from": "+15551234567", "to": "+15557654321", "body": "Your appointment is confirmed for 2pm — reply STOP to opt out.", "senderName": "Appointment Reminder Bot", }, ) message = res.json() ``` Numbers use [E.164 format](/developers/glossary/e164) — a leading `+` and country code, e.g. `+15551234567`. Opt-out is handled automatically: recipients who have texted `STOP` are suppressed before delivery. See [Opt-out](/developers/docs/opt-out). ### Request fields | Field | Type | Required | Description | | ----------- | ---------- | -------- | ------------------------------------------------------ | | `from` | `string` | yes | An [E.164](/developers/glossary/e164) number you own. | | `to` | `string` | yes | Recipient in E.164 format. | | `body` | `string` | yes | Message text. | | `senderName` | `string` | no | Name identifying the message source in the softphone inbox — a team member's name or an automation/script name. See [Sender identification](#sender-identification). | | `mediaUrls` | `string[]` | no | Media URLs to attach — sends as [MMS](#mms). | ## The message object A successful send returns the created message. The same shape is returned by `GET /messages/{id}`. ```json { "id": "msg_01h…", "from": "+15551234567", "to": "+15557654321", "body": "Your appointment is confirmed for 2pm — reply STOP to opt out.", "direction": "outbound", "status": "queued", "createdAt": "2026-05-31T14:02:09Z" } ``` | Field | Type | Description | | ----------- | -------- | --------------------------------------------------------------------------------- | | `id` | `string` | Unique message identifier, e.g. `msg_01h…`. Use it to look the message up later. | | `from` | `string` | The sending E.164 number. | | `to` | `string` | The recipient E.164 number. | | `body` | `string` | The message text. | | `direction` | `string` | `outbound` for messages you send, `inbound` for messages you receive. | | `status` | `string` | Lifecycle state — one of `queued`, `sent`, `delivered`, `failed`. | | `createdAt` | `string` | ISO 8601 timestamp of when the message was created. | ## MMS To send an MMS, include a `mediaUrls` array alongside `body`. Each entry is a publicly reachable URL to the media you want to attach; `body` is optional when media is present. **curl** ```bash curl https://api.netexem.com/v1/messages \ -H "Authorization: Bearer $NETEXEM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+15551234567", "to": "+15557654321", "body": "Here is your receipt.", "mediaUrls": ["https://files.example.com/receipts/inv-1042.pdf"] }' ``` **Node** ```ts const res = await fetch("https://api.netexem.com/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.NETEXEM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: "+15551234567", to: "+15557654321", body: "Here is your receipt.", mediaUrls: ["https://files.example.com/receipts/inv-1042.pdf"], }), }); const message = await res.json(); ``` **Python** ```python import os, requests res = requests.post( "https://api.netexem.com/v1/messages", headers={"Authorization": f"Bearer {os.environ['NETEXEM_API_KEY']}"}, json={ "from": "+15551234567", "to": "+15557654321", "body": "Here is your receipt.", "mediaUrls": ["https://files.example.com/receipts/inv-1042.pdf"], }, ) message = res.json() ``` ## Sender identification The optional `senderName` field identifies who or what sent the message, displayed in the softphone inbox. It supports two primary use cases: ### For manual sends — team member names When a team member sends a message, populate `senderName` with their name: ```json { "from": "+15551234567", "to": "+15557654321", "body": "Hi! Just checking in on your order.", "senderName": "Sarah Chen" } ``` The message appears in the softphone inbox labeled with Sarah's name, so your team can trace it back to a specific person. ### For automated sends — script and workflow names When an automation, script, or workflow sends a message, use a descriptive name that identifies the automation: ```json { "from": "+15551234567", "to": "+15557654321", "body": "Your appointment reminder: Tomorrow at 2:00 PM", "senderName": "Appointment Reminder Bot" } ``` Other examples of automation names: - `"Lead Follow-Up Sequence"` — for CRM drip campaigns - `"Post-Sale Survey"` — for automated customer feedback - `"Re-Engagement Campaign"` — for win-back workflows - `"Notification System"` — for alert automations ### Why this matters When multiple automations are running in parallel (appointment reminders, follow-up sequences, drip campaigns), the `senderName` becomes the primary way your team identifies which workflow or script triggered each message. Without clear sender identification, it's difficult to: - **Audit** which automation sent a given message to a customer - **Troubleshoot** unexpected messaging behavior - **Report** on engagement by automation or workflow If `senderName` is omitted, the message is attributed to the `from` number alone. ## Delivery status The `status` on the returned object is its state at the moment of the request — almost always `queued`. Delivery is asynchronous, so the status advances after the response: `queued` → `sent` → `delivered`, or `failed` if the carrier rejects it. Track those transitions with **delivery-receipt webhooks**. Subscribe an endpoint and you'll receive a signed event each time a message advances, so you can reconcile state without polling. Every event is HMAC-SHA256 signed in the `X-Netexem-Signature` header — verify it before trusting the payload. See [Webhooks](/developers/docs/webhooks) for the event shape and signature verification. Every message you send also appears in the **softphone inbox** in real time — your team sees the same conversation thread your code does, with no extra setup. If you need to read a single message's current state directly, fetch it: ```bash curl https://api.netexem.com/v1/messages/msg_01h… \ -H "Authorization: Bearer $NETEXEM_API_KEY" ``` ## Compliance - **Opt-out is automatic.** Recipients who reply `STOP` are suppressed for the relevant number, and further sends to them are blocked before they reach the carrier. Honor and surface these states rather than working around them. See [Opt-out](/developers/docs/opt-out). - **Use E.164 everywhere.** Both `from` and `to` must be [E.164](/developers/glossary/e164); malformed numbers return `422`. - **Send from numbers you own.** `from` must be a provisioned number on your account. ## Next steps - [Webhooks](/developers/docs/webhooks) — delivery receipts and inbound `message.received` events - [Opt-out](/developers/docs/opt-out) — how STOP/START is handled for you - [API reference](/developers/docs/api-reference) — try `POST /messages` live --- Source: https://vancetel.com/developers/docs/group-sms Markdown: https://vancetel.com/developers/docs/group-sms.md # Group SMS Group SMS lets you open one conversation with several recipients from a single business number. Every reply threads back into that same conversation, addressable by a stable `conversationId`, so your software — and the softphone inbox — sees one thread instead of a scatter of one-to-one messages. Group conversations follow the same write- and event-oriented model as a single SMS: you create the conversation and send into it, then subscribe to [Webhooks](/developers/docs/webhooks) for inbound replies and per-participant delivery. ## Create a group Open a group conversation by sending the first message with more than one recipient in `to`. The `from` number must be an [E.164](/developers/glossary/e164) number you own; each recipient is also E.164. The response returns a `conversationId` — store it. Everything that follows in this thread is keyed to it. **curl** ```bash curl https://api.netexem.com/v1/messages \ -H "Authorization: Bearer $NETEXEM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "+15551234567", "to": ["+15557654321", "+15557654322", "+15557654323"], "body": "Kickoff call moved to 3pm — works for everyone?" }' ``` **Node** ```ts const res = await fetch("https://api.netexem.com/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.NETEXEM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: "+15551234567", to: ["+15557654321", "+15557654322", "+15557654323"], body: "Kickoff call moved to 3pm — works for everyone?", }), }); const message = await res.json(); const conversationId = message.conversationId; ``` **Python** ```python import os, requests res = requests.post( "https://api.netexem.com/v1/messages", headers={"Authorization": f"Bearer {os.environ['NETEXEM_API_KEY']}"}, json={ "from": "+15551234567", "to": ["+15557654321", "+15557654322", "+15557654323"], "body": "Kickoff call moved to 3pm — works for everyone?", }, ) message = res.json() conversation_id = message["conversationId"] ``` All numbers use [E.164 format](/developers/glossary/e164) — a leading `+` and country code, e.g. `+15557654321`. A single recipient (a plain string `to`) opens a one-to-one thread instead; see [Send an SMS](/developers/docs/sms). To send another message into an existing group, target the conversation by its id rather than re-listing recipients: ```json { "conversationId": "cv_01h…", "body": "Confirmed — invite updated." } ``` ## Replies & threading When any participant texts back, the reply threads into the same conversation. We fire a signed `message.received` webhook carrying the originating `conversationId`, so you can correlate it without guessing. The softphone surfaces the same thread in real time. A group conversation object collects the participants, the originating number, and message status. Look it up with `GET /messages/{id}` for an individual message, or read the `conversationId` carried on every related message and webhook to stitch the thread together. ```json { "conversationId": "cv_01h…", "from": "+15551234567", "participants": [ { "phone": "+15557654321", "status": "delivered" }, { "phone": "+15557654322", "status": "delivered" }, { "phone": "+15557654323", "status": "queued" } ], "messages": [ { "id": "msg_01h…", "from": "+15551234567", "body": "Kickoff call moved to 3pm — works for everyone?", "direction": "outbound", "status": "sent", "createdAt": "2026-05-31T18:04:21Z" }, { "id": "msg_01h…", "from": "+15557654321", "body": "Works for me.", "direction": "inbound", "status": "delivered", "createdAt": "2026-05-31T18:05:02Z" } ], "createdAt": "2026-05-31T18:04:21Z" } ``` Each inbound reply arrives as its own `message.received` event tagged with the `conversationId`. Acknowledge with a `2xx`; non-`2xx` responses are retried with backoff. See [Webhooks](/developers/docs/webhooks). ## Participants Add or remove participants without losing the thread — the `conversationId` stays stable across changes. - **Add** a participant by including their E.164 number when you send into the conversation; new participants join the existing thread. - **Remove** a participant to stop including them on subsequent messages. History already delivered to them is unaffected. ```json { "conversationId": "cv_01h…", "addParticipants": ["+15557654324"], "removeParticipants": ["+15557654323"], "body": "Adding Priya, dropping the old thread." } ``` Delivery is tracked **per participant**: each entry in `participants` carries its own `status` (`queued`, `sent`, `delivered`, `failed`), so one unreachable number never blocks the rest of the group. Watch delivery transitions through delivery-receipt [Webhooks](/developers/docs/webhooks), each keyed to the conversation and the participant it concerns. A participant who replies STOP is opted out and removed from future sends to the group automatically. Opt-out is honored per number, account-wide — see [Opt-out & compliance](/developers/docs/opt-out). ## Next steps - [Send an SMS](/developers/docs/sms) — the full message object, MMS and delivery receipts - [Webhooks](/developers/docs/webhooks) — subscribe to inbound replies and per-participant delivery - [Opt-out & compliance](/developers/docs/opt-out) — STOP handling and A2P registration --- Source: https://vancetel.com/developers/docs/opt-out Markdown: https://vancetel.com/developers/docs/opt-out.md # Opt-out & compliance Every number you send from honors SMS opt-out automatically. The API handles the standard STOP/START/HELP keywords for you, blocks sends to anyone who has opted out, and gives you the registration context you need to run [two-way SMS](/developers/glossary/two-way-sms) compliantly. SMS compliance is not optional. In the US, the TCPA and carrier [A2P 10DLC](/developers/glossary/a2p-10dlc) rules govern business messaging — unregistered or non-compliant traffic gets filtered, blocked, or fined. Treat opt-out handling as a hard requirement, not a feature. ## Automatic keyword handling Inbound messages matching the standard keywords are intercepted and acted on before they reach your application: - **STOP** (and equivalents like `STOPALL`, `UNSUBSCRIBE`, `CANCEL`, `END`, `QUIT`) — the recipient is opted out. We send the carrier-mandated confirmation and block further sends to that number on the account. - **START** (or `UNSTOP`, `YES`) — the recipient opts back in. Sending resumes. - **HELP** (or `INFO`) — the recipient receives your help reply identifying your business and support contact. You still receive a signed `message.received` webhook for these so your records stay in sync, but you do not need to implement the keyword logic yourself. See [Webhooks](/developers/docs/webhooks). ## Blocked recipients are rejected before send Once a number has opted out, any attempt to message it is rejected **before** the message leaves the platform — nothing is delivered and you are not billed for it. The send fails validation with a `422` and a clear error. ```json { "error": { "type": "recipient_opted_out", "message": "+15557654321 has opted out and cannot be messaged." } } ``` This applies everywhere a number is a recipient, including [Group SMS](/developers/docs/group-sms): an opted-out participant is dropped from the send while the rest of the group still receives the message. ## TCPA & A2P 10DLC Two layers of rules apply to US business SMS: - **TCPA** — federal law requiring prior express consent before sending, a clear way to opt out, and honoring opt-outs promptly. It carries statutory damages per message, so consent and recordkeeping matter. - **A2P 10DLC** — the carrier framework for application-to-person traffic on standard 10-digit long codes. You register your business (brand) and each messaging use case (campaign) before sending. Registration determines your throughput and deliverability; unregistered traffic is heavily filtered. Register your brand and campaigns during onboarding before sending live business traffic. The sandbox and test keys let you build and test against test numbers without registration — see [Authentication](/developers/docs/authentication). ## Best practices - **Get consent first.** Only message numbers that opted in to your specific use case. Keep a record of when and how. - **Include opt-out language.** State how to stop, e.g. _"Reply STOP to opt out,"_ especially in the first message of a conversation. - **Identify yourself.** Make it clear which business is texting, in the body and in your HELP reply. - **Honor requests immediately.** Automatic STOP handling covers the keywords; honor any opt-out conveyed another way (a phone call, an email) just as fast. - **Match content to your registered campaign.** Sending content outside your registered A2P use case risks filtering and suspension. Because opt-out, START, and HELP are handled at the platform level and blocked recipients are rejected before send, the core carrier-keyword obligations are covered for you — leaving consent, identification, and registration as your responsibility. ## Next steps - [Send an SMS](/developers/docs/sms) — message object and delivery receipts - [Group SMS](/developers/docs/group-sms) — per-participant opt-out in group threads - [Webhooks](/developers/docs/webhooks) — receive STOP/START/HELP and delivery events --- Source: https://vancetel.com/developers/docs/deep-links Markdown: https://vancetel.com/developers/docs/deep-links.md # Deep links Deep links are URLs that open the softphone with an action ready to go — a call to a number, or a new text addressed to a recipient. Drop them on any button, CRM record or dashboard so your team can start a conversation in one tap, without copying numbers by hand. Unlike the REST API, deep links don't send anything on their own. They hand off to the softphone app, which opens the dialer or composer pre-filled. The person still presses call or send. A deep link never sends anything by itself. A text link can pre-fill the **recipient** and, optionally, a **draft message body** — but that draft opens in the composer, editable, and is **never auto-sent**. The user reviews and taps Send. To send a message programmatically, use the [Send an SMS](/developers/docs/sms) endpoint instead. ## Format ```text netexem://?to=[&from=][&ad=<0|1>][&body=] ``` | Param | Required | Notes | |--------|----------|-------| | `to` | yes | Recipient in [E.164](/developers/glossary/e164). URL-encode the `+` as `%2B`. | | `from` | no | For `sms`, selects which of your numbers to send from; defaults to your default line. For `call` it is parsed but ignored today. | | `ad` | no — `call` only | `1` auto-dials immediately; `0`/absent shows a confirmation prompt (the safer default). | | `body` | no — `sms` only | URL-encoded draft message (soft cap ~1000 chars). Pre-fills the composer, editable, never auto-sent. Ignored on `call`. | The only valid actions are `call` and `sms`. Any unknown action or malformed `to` is silently dropped (fails safe). ## Launch a call A call deep link opens the softphone dialer with the recipient set, ready to dial. The number uses [E.164 format](/developers/glossary/e164). ```text netexem://call?to=+15557654321 ``` Use this on a "Call" button next to any phone number. The user taps, the dialer opens pre-addressed, and they press call. ## Launch a pre-addressed text A text deep link uses the `sms` action and opens the message composer addressed to the recipient. With just `to`, the message field is left empty for the user to type. ```text netexem://sms?to=+15557654321 ``` ## Pre-fill a draft message Add an optional `body` parameter to pre-fill the composer with draft text. The user still reviews, edits and taps Send — nothing is auto-sent. ```text netexem://sms?to=+15557654321&body=Hi%20there%2C%20your%20order%20%23123%20is%20ready ``` URL-encode the body with `encodeURIComponent` (JS) or `urllib.parse.quote` (Python): space → `%20`, newline → `%0A`, `&` → `%26`, `#` → `%23`. The body is a soft `~1000`-character cap (longer text is truncated) and is **ignored on `call`** links. `body` only applies to the `sms` action and only pre-fills the composer — it is never auto-sent. To send a message without a human in the loop, use the [Send an SMS](/developers/docs/sms) endpoint. ## Use cases Deep links shine anywhere a human is about to start a conversation from your software: - **CRM record** — render a call link next to each contact's number so a rep dials straight from the lead view. - **Dashboard button** — add a "Text customer" button on an order or ticket that opens the composer addressed to them. - **Support tool** — let an agent jump from a help-desk ticket into a call without retyping the number. These pair naturally with [Contacts](/developers/docs/contacts): create the contact so the softphone resolves caller ID, then deep-link the call or text to that person's number. ## Building a link in your app Construct the URL with the recipient as the `to` query parameter. URL-encode the value so the leading `+` survives. **Node** ```ts function callLink(to) { return `netexem://call?to=${encodeURIComponent(to)}`; } // body is optional — omit it to open an empty composer function textLink(to, body) { const draft = body ? `&body=${encodeURIComponent(body)}` : ""; return `netexem://sms?to=${encodeURIComponent(to)}${draft}`; } textLink("+15557654321", "Hi there, your order #123 is ready"); // netexem://sms?to=%2B15557654321&body=Hi%20there%2C%20your%20order%20%23123%20is%20ready ``` **Python** ```python from urllib.parse import quote def call_link(to: str) -> str: return f"netexem://call?to={quote(to)}" # body is optional — omit it to open an empty composer def text_link(to: str, body: str | None = None) -> str: draft = f"&body={quote(body)}" if body else "" return f"netexem://sms?to={quote(to)}{draft}" text_link("+15557654321", "Hi there, your order #123 is ready") # netexem://sms?to=%2B15557654321&body=Hi%20there%2C%20your%20order%20%23123%20is%20ready ``` Render the result as an ordinary anchor in your UI: ```html Call Jane Text Jane Text Jane (with draft) ``` ## Next steps - [Contacts](/developers/docs/contacts) — create a contact so caller ID resolves - [Send an SMS](/developers/docs/sms) — send a message programmatically, body and all - [API reference](/developers/docs/api-reference) — every endpoint, with a live console --- Source: https://vancetel.com/developers/docs/contacts Markdown: https://vancetel.com/developers/docs/contacts.md # Contacts Contacts let you push people into the shared softphone directory from your own software. A contact created through the API resolves caller ID across the softphone: when that number calls or texts, your team sees the name, company and details instead of a raw number. Contacts are a **write- and lookup-oriented** resource. You create a contact and you retrieve it by id. The softphone app surfaces the full directory and conversation history in real time. ## Create a contact A single `POST` to `/v1/contacts` adds a person to the directory. Only `phone` is required; `name`, `email` and `company` are optional but improve caller-ID resolution. **curl** ```bash curl https://api.netexem.com/v1/contacts \ -H "Authorization: Bearer $NETEXEM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Jane Doe", "phone": "+15557654321", "email": "jane@example.com", "company": "Acme Co" }' ``` **Node** ```ts const res = await fetch("https://api.netexem.com/v1/contacts", { method: "POST", headers: { Authorization: `Bearer ${process.env.NETEXEM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Jane Doe", phone: "+15557654321", email: "jane@example.com", company: "Acme Co", }), }); const contact = await res.json(); ``` **Python** ```python import os, requests res = requests.post( "https://api.netexem.com/v1/contacts", headers={"Authorization": f"Bearer {os.environ['NETEXEM_API_KEY']}"}, json={ "name": "Jane Doe", "phone": "+15557654321", "email": "jane@example.com", "company": "Acme Co", }, ) contact = res.json() ``` Phone numbers use [E.164 format](/developers/glossary/e164) — a leading `+` and country code, e.g. `+15557654321`. ## The contact object A successful create returns `201` with the stored contact. The same shape is returned when you retrieve a contact later. ```json { "id": "ct_01h…", "name": "Jane Doe", "phone": "+15557654321", "email": "jane@example.com", "company": "Acme Co", "createdAt": "2026-05-31T14:02:11Z" } ``` | Field | Type | Notes | | --- | --- | --- | | `id` | string | Unique contact id, prefixed `ct_`. | | `name` | string | Display name shown for caller ID. | | `phone` | string | The contact's number in [E.164](/developers/glossary/e164). | | `email` | string | Optional email address. | | `company` | string | Optional company name. | | `createdAt` | string | ISO 8601 timestamp of creation. | ## From a web form A common pattern: a visitor submits a form on your website, your server creates a contact, and the next time that person calls or texts the softphone already knows who they are. The flow is two hops — the browser posts the form to **your** server, and your server (where the API key lives) creates the contact. Never call the API directly from the browser; that would expose your key. See [Authentication](/developers/docs/authentication). **Node** ```ts // POST /lead — your server's form handler app.post("/lead", async (req, res) => { const { name, phone, email, company } = req.body; // Create the contact so the softphone resolves caller ID await fetch("https://api.netexem.com/v1/contacts", { method: "POST", headers: { Authorization: `Bearer ${process.env.NETEXEM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ name, phone, email, company }), }); res.redirect("/thanks"); }); ``` **Python** ```python import os, requests from flask import request, redirect # POST /lead — your server's form handler @app.post("/lead") def lead(): payload = { "name": request.form["name"], "phone": request.form["phone"], "email": request.form.get("email"), "company": request.form.get("company"), } # Create the contact so the softphone resolves caller ID requests.post( "https://api.netexem.com/v1/contacts", headers={"Authorization": f"Bearer {os.environ['NETEXEM_API_KEY']}"}, json=payload, ) return redirect("/thanks") ``` Once created, the contact is shared across the softphone. An inbound call or text from that number now shows the person's name and company instead of a bare number. To follow up immediately, pair this with a [deep link](/developers/docs/deep-links) on your dashboard, or send a confirmation text — see [Send an SMS](/developers/docs/sms). ## Retrieve a contact Fetch a contact by id with a `GET` to `/v1/contacts/{id}`. Store the `id` from the create response to look it up later. ```bash curl https://api.netexem.com/v1/contacts/ct_01h… \ -H "Authorization: Bearer $NETEXEM_API_KEY" ``` A successful lookup returns `200` with the [contact object](#the-contact-object). An unknown id returns `404 Not Found` — see [Errors](/developers/docs/errors) for the full error shape. ## Next steps - [Deep links](/developers/docs/deep-links) — launch a call or pre-addressed text to a contact - [Webhooks](/developers/docs/webhooks) — react to inbound messages and call events - [API reference](/developers/docs/api-reference) — try the contacts endpoints live --- Source: https://vancetel.com/developers/docs/webhooks Markdown: https://vancetel.com/developers/docs/webhooks.md # Webhooks Webhooks push real-time events to your server. Where the rest of the API is something **you** call, webhooks are how **we** notify you: an inbound text arrives, a message is delivered or fails, a call wraps up. Each event is an HTTP `POST` to an endpoint you control, signed so you can prove it came from us. Webhooks are the inbound half of the platform. The API is write- and action-oriented (you send messages and create contacts); webhooks deliver everything that happens back — see the [Introduction](/developers/docs). ## Subscribing to events Register an HTTPS endpoint from your dashboard and select the event types you want. Two rules apply to every endpoint: - It must be reachable over **HTTPS** at a public URL. - It must respond with a `2xx` status quickly (see [Retries and backoff](#retries-and-backoff)). When you create the endpoint we issue a **signing secret** (`whsec_…`). Store it server-side and use it to verify every delivery. Treat it like an API key — see [Authentication](/developers/docs/authentication). ## The event envelope Every webhook shares one envelope. The `type` tells you what happened, `createdAt` is when it happened in UTC, and `data` carries the event-specific object. ```json { "type": "message.received", "createdAt": "2026-05-31T17:04:22Z", "data": { "id": "msg_01h…", "from": "+15557654321", "to": "+15551234567", "body": "Yes, 2pm works", "direction": "inbound", "status": "delivered" } } ``` The shape of `data` depends on `type`. For message events it is the [Message](/developers/docs/sms) object; for call events it is a call summary. Always branch on `type` before reading `data`. ## Event catalog | Event | When it fires | `data` payload | | --- | --- | --- | | `message.received` | An inbound SMS or MMS arrives at one of your numbers. | The inbound Message (`direction: "inbound"`). | | `message.delivered` | The carrier confirms an outbound message reached the handset. | The Message with `status: "delivered"`. | | `message.failed` | An outbound message could not be delivered (bad number, carrier rejection, opt-out). | The Message with `status: "failed"`. | | `call.completed` | A call ends. | A call summary with the two parties, direction, start time and duration. | | `call.missed` | An inbound call is not answered. | A call summary with `status: "missed"`. | | `contact.created` | A contact is created (via the API or a contact form). | The new Contact object. | `message.received` is the one most integrations start with — it's how you build auto-replies and route inbound texts. Pair it with `message.delivered` and `message.failed` to track outbound send outcomes without polling. ## Verifying the signature We sign every request with HMAC-SHA256 over the timestamp joined to the **raw request body**, keyed with your endpoint's signing secret. The `X-Netexem-Signature` header carries both parts as `t=,v1=`. Split out `v1`, recompute the HMAC over `t` + `"."` + the raw body, and compare against it using a **timing-safe** comparison. Reject stale timestamps to blunt replays. Verify the signature **before** you trust or act on a payload. An unverified request can be forged by anyone who knows your URL. Compute HMAC over the exact raw bytes you received — parse the JSON only after the signature checks out — and reject anything that doesn't match with a `400`. **Node** ```ts import crypto from "node:crypto"; // rawBody must be the exact bytes received, NOT a re-serialized object. // Header looks like: t=1717000000,v1=8a1f...c4 function verifyWebhook(rawBody, signatureHeader, signingSecret) { const parts = Object.fromEntries( (signatureHeader ?? "").split(",").map((kv) => kv.split("=")), ); const { t: ts, v1: sig } = parts; if (!ts || !sig) return false; const expected = crypto .createHmac("sha256", signingSecret) .update(ts + "." + rawBody) .digest("hex"); const a = Buffer.from(sig); const b = Buffer.from(expected); // Lengths must match before timingSafeEqual, or it throws. return a.length === b.length && crypto.timingSafeEqual(a, b); } // Express: capture the raw body so the signature stays byte-exact. const app = express(); app.post( "/webhooks/netexem", express.raw({ type: "application/json" }), (req, res) => { const ok = verifyWebhook( req.body, // Buffer of raw bytes req.get("X-Netexem-Signature"), process.env.NETEXEM_WEBHOOK_SECRET, ); if (!ok) return res.status(400).send("invalid signature"); const event = JSON.parse(req.body.toString("utf8")); // ...handle event.type, then acknowledge res.sendStatus(200); }, ); ``` **Python** ```python import hashlib def verify_webhook(raw_body: bytes, signature_header: str, signing_secret: str) -> bool: # Header looks like: t=1717000000,v1=8a1f...c4 parts = dict(p.split("=", 1) for p in (signature_header or "").split(",")) ts, sig = parts.get("t"), parts.get("v1") if not ts or not sig: return False signed = ts.encode() + b"." + raw_body # exact bytes received, not re-serialized expected = hmac.new(signing_secret.encode(), signed, hashlib.sha256).hexdigest() # Constant-time comparison guards against timing attacks. return hmac.compare_digest(expected, sig) # Flask: request.get_data() returns the raw bytes. from flask import Flask, request, abort app = Flask(__name__) @app.post("/webhooks/netexem") def netexem_webhook(): raw = request.get_data() ok = verify_webhook( raw, request.headers.get("X-Netexem-Signature"), os.environ["NETEXEM_WEBHOOK_SECRET"], ) if not ok: abort(400) event = request.get_json() # ...handle event["type"], then acknowledge return "", 200 ``` ## Retries and backoff We treat any `2xx` response as a successful acknowledgement. Anything else — a non-`2xx` status, a connection error, or a timeout — is considered a failure and the event is **retried with exponential backoff** over an extended window. Repeated failures eventually stop and the event is marked undelivered. Return your `2xx` **quickly**. Do the minimum to confirm receipt — verify the signature, enqueue or persist the event — then respond. Run slow work (database writes, third-party calls, sending replies) asynchronously after you've acknowledged. If your handler does heavy work inline, a slow run can trip our timeout and trigger a retry — so you process the same event twice. Acknowledge fast and process out of band to avoid it. ## Idempotency Because retries can deliver the same event more than once, your handler must be **idempotent**. Every event carries a stable `id` in `data` (for example `msg_01h…`). Record the ids you've already processed and skip duplicates: ```ts async function handleEvent(event: { type: string; data: { id: string } }) { const seen = await store.has(event.data.id) if (seen) return // already processed — safe to ignore await process(event) await store.add(event.data.id) } ``` Deduplicating on the event id means a retried delivery is a no-op, so backoff retries never double-charge, double-reply, or double-write. ## Next steps - [Send an SMS](/developers/docs/sms) — the Message object you'll receive in message events - [Authentication](/developers/docs/authentication) — how API keys and the webhook signing secret differ - [Errors](/developers/docs/errors) — the error shape returned by the API you call --- Source: https://vancetel.com/developers/docs/errors Markdown: https://vancetel.com/developers/docs/errors.md # Errors The API uses conventional HTTP status codes to signal success or failure, and returns a consistent JSON **error object** on every failed request. Read the status code first to decide what to do, then read the error body for detail. ## The error object Every error response has the same shape: a top-level `error` object with a machine-readable `type` and a human-readable `message`. ```json { "error": { "type": "invalid_request", "message": "field 'to' must be an E.164 phone number" } } ``` - **`type`** — a stable string you can branch on in code (e.g. `invalid_request`). Match on this, not on the message text. - **`message`** — a description meant for logs and developers. It is not localized and may change; never parse it. Branch your code on the HTTP status code and on `error.type`. Treat `error.message` as a human-readable hint only — its wording is not part of the contract. ## Status codes | Status | Meaning | When it occurs | | --- | --- | --- | | `200 OK` | Success | A `GET` succeeded, e.g. retrieving a [message](/developers/docs/sms) or [contact](/developers/docs/contacts). | | `201 Created` | Success | A resource was created, e.g. a message was accepted for delivery or a contact was created. | | `400 Bad Request` | Client error | The request was malformed — invalid JSON, or a missing/unsupported `Content-Type`. | | `401 Unauthorized` | Auth error | The API key is missing, malformed, or revoked. See [Authentication](/developers/docs/authentication). | | `404 Not Found` | Client error | The resource ID does not exist or is not on your account (e.g. an unknown message ID). | | `422 Unprocessable Entity` | Validation error | The request was well-formed but failed validation — e.g. a non-[E.164](/developers/glossary/e164) number or an empty `body`. | | `429 Too Many Requests` | Rate limited | You exceeded your request rate. Back off and retry — see [Rate limits](/developers/docs/rate-limits). | | `500`–`503` | Server error | Something went wrong on our side. These are transient; retry with backoff. | A `2xx` for `POST /messages` means the message was **accepted for delivery**, not that it has reached the handset. Final delivery is reported asynchronously via the message `status` field and delivery-receipt [webhooks](/developers/docs/webhooks). ## Example responses A validation failure on `POST /messages` returns `422` with a `validation_error` type: ```json { "error": { "type": "validation_error", "message": "field 'to' is required" } } ``` A missing or revoked key returns `401`: ```json { "error": { "type": "unauthorized", "message": "missing or invalid API key" } } ``` Requesting an unknown ID from `GET /messages/{id}` returns `404`: ```json { "error": { "type": "not_found", "message": "no message with id 'msg_does_not_exist'" } } ``` ## Handling errors Check the status code, then read `error.type` for cases you handle explicitly. The pattern below treats `4xx` as caller mistakes to fix, `429` and `5xx` as retryable, and surfaces the error object on everything else. **Node** ```ts async function sendMessage(payload) { const res = await fetch("https://api.netexem.com/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.NETEXEM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (res.ok) return res.json(); const { error } = await res.json(); if (res.status === 401) { throw new Error(`Auth failed: ${error.message}`); } if (res.status === 422 || res.status === 400) { // Caller mistake — fix the request, do not retry as-is. throw new Error(`Invalid request (${error.type}): ${error.message}`); } if (res.status === 429 || res.status >= 500) { // Transient — safe to retry with backoff. See /docs/rate-limits. throw new Error(`Retryable error (${res.status}): ${error.message}`); } throw new Error(`Unexpected ${res.status}: ${error.message}`); } ``` **Python** ```python import os, requests def send_message(payload): res = requests.post( "https://api.netexem.com/v1/messages", headers={"Authorization": f"Bearer {os.environ['NETEXEM_API_KEY']}"}, json=payload, ) if res.ok: return res.json() error = res.json()["error"] if res.status_code == 401: raise RuntimeError(f"Auth failed: {error['message']}") if res.status_code in (400, 422): # Caller mistake — fix the request, do not retry as-is. raise ValueError(f"Invalid request ({error['type']}): {error['message']}") if res.status_code == 429 or res.status_code >= 500: # Transient — safe to retry with backoff. See /docs/rate-limits. raise RuntimeError(f"Retryable error ({res.status_code}): {error['message']}") raise RuntimeError(f"Unexpected {res.status_code}: {error['message']}") ``` ### Which errors to retry - **Do not retry** `400`, `401`, `404`, or `422` — the request will fail again until you fix it. - **Retry** `429` and `5xx` with exponential backoff. For `429`, honor the `Retry-After` header. See [Rate limits](/developers/docs/rate-limits) for the recommended loop. Log the full error object (`type` and `message`) alongside the request ID from your own system. The `type` lets you aggregate failures; the `message` speeds up debugging. ## Next steps - [Rate limits](/developers/docs/rate-limits) — handling `429` and backing off correctly - [Authentication](/developers/docs/authentication) — avoiding `401` errors - [API reference](/developers/docs/api-reference) — try requests live and see real responses --- Source: https://vancetel.com/developers/docs/rate-limits Markdown: https://vancetel.com/developers/docs/rate-limits.md # Rate limits Requests are rate limited **per API key**. When you exceed your limit, the API returns `429 Too Many Requests` with a `Retry-After` header telling you how long to wait. Build a small backoff loop and you will rarely notice the ceiling. ## How limits work - Limits are applied **per API key**, so test and live keys are metered independently. - Limits are measured as a request rate (requests per second / per minute) with short bursts allowed above the steady rate. - When you go over, further requests return `429` until the window refreshes. Indicative steady-state limits are on the order of a few requests per second per key, with short bursts allowed. These figures are **indicative, not guarantees** — your account's effective limits depend on your plan and traffic history. Design for backoff rather than a fixed number. ## The 429 response A throttled request returns `429` with the standard [error object](/developers/docs/errors) and a `Retry-After` header. `Retry-After` is the number of seconds to wait before retrying. ```http HTTP/1.1 429 Too Many Requests Retry-After: 2 Content-Type: application/json { "error": { "type": "rate_limited", "message": "too many requests — retry after 2 seconds" } } ``` ## Recommended client behavior 1. **Respect `Retry-After` first.** When present, wait at least that many seconds before retrying. 2. **Fall back to exponential backoff** when `Retry-After` is absent (e.g. on a `5xx`): wait 1s, 2s, 4s, 8s, doubling each attempt. 3. **Add jitter** — a small random offset on each delay so concurrent clients do not retry in lockstep. 4. **Cap retries** — give up after a few attempts and surface the failure rather than looping forever. 5. **Smooth your own send rate** — spread bulk sends over time instead of firing them all at once. Never retry a `4xx` other than `429`. A `400`, `401`, `404`, or `422` will keep failing until you fix the request — see [Errors](/developers/docs/errors). ## Retry-on-429 loop This loop retries on `429` and `5xx`, honors `Retry-After` when present, and otherwise backs off exponentially with jitter. **Node** ```ts async function sendWithRetry(payload, maxRetries = 5) { for (let attempt = 0; attempt <= maxRetries; attempt++) { const res = await fetch("https://api.netexem.com/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.NETEXEM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (res.ok) return res.json(); // Only retry on rate limits and server errors. if (res.status !== 429 && res.status < 500) { const { error } = await res.json(); throw new Error(`${res.status} ${error.type}: ${error.message}`); } if (attempt === maxRetries) { throw new Error(`Gave up after ${maxRetries} retries (last status ${res.status})`); } // Honor Retry-After, else exponential backoff with jitter. const retryAfter = Number(res.headers.get("Retry-After")); const backoff = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 1000; const jitter = Math.random() * 250; await new Promise((r) => setTimeout(r, backoff + jitter)); } } ``` **Python** ```python import os, time, random, requests def send_with_retry(payload, max_retries=5): for attempt in range(max_retries + 1): res = requests.post( "https://api.netexem.com/v1/messages", headers={"Authorization": f"Bearer {os.environ['NETEXEM_API_KEY']}"}, json=payload, ) if res.ok: return res.json() # Only retry on rate limits and server errors. if res.status_code != 429 and res.status_code < 500: error = res.json()["error"] raise RuntimeError(f"{res.status_code} {error['type']}: {error['message']}") if attempt == max_retries: raise RuntimeError( f"Gave up after {max_retries} retries (last status {res.status_code})" ) # Honor Retry-After, else exponential backoff with jitter. retry_after = res.headers.get("Retry-After") backoff = float(retry_after) if retry_after else 2 ** attempt time.sleep(backoff + random.uniform(0, 0.25)) ``` ## Carrier and A2P limits Request-rate limits are not the only ceiling on messaging. Outbound SMS/MMS throughput is also bounded by **carrier and A2P (application-to-business) limits** — per-number sending rates and registered campaign throughput set by the carriers, independent of the API. - These limits apply even when you are well under your API request rate. - They protect deliverability and keep your numbers in good standing. - Honoring opt-outs is part of staying within them — see [Opt-out handling](/developers/docs/opt-out). For high-volume sending, queue messages on your side and drain the queue at a steady rate. This keeps you under both the API request limit and carrier throughput limits, and makes bursts easy to absorb. ## Next steps - [Errors](/developers/docs/errors) — every status code and the error object shape - [Opt-out handling](/developers/docs/opt-out) — staying compliant with carrier rules - [Webhooks](/developers/docs/webhooks) — delivery receipts so you know what actually sent --- Source: https://vancetel.com/developers/docs/mcp Markdown: https://vancetel.com/developers/docs/mcp.md # MCP server The VanceTel **MCP server** exposes the phone system to AI agents over the [Model Context Protocol](/developers/glossary/mcp-server). Connect it to Claude Code, Claude Desktop, or any MCP-compatible agent and the model can send a text, look up or create a contact, and check a message's status as tool calls inside its own loop — using the same API key and the same actions you would call over REST. The MCP server is a thin, agent-facing wrapper over the public API. It performs the same write- and action-oriented operations and respects the same key scopes — it does not expose anything the REST API doesn't. ## What an agent can do Once connected, an agent can drive the phone system without leaving its conversation: - **Send a text** — dispatch an SMS/MMS from one of your numbers (mirrors `POST /messages`). - **Look up a contact** — resolve a person by phone or name for caller ID and context. - **Create a contact** — add someone to the shared softphone directory on the fly (mirrors `POST /contacts`). - **Check a message's status** — fetch a message by id to see whether it delivered, so the agent can decide what to do next. Everything the agent does also appears in the softphone app in real time, so a human stays in the loop. ## Connect the server Add an `mcpServers` entry to your MCP client config. The example below is illustrative — pass your API key through the environment, never inline in shared config. ```json { "mcpServers": { "netexem": { "command": "npx", "args": ["-y", "@netexem/mcp-server"], "env": { "NETEXEM_API_KEY": "sk_live_…" } } } } ``` A remote (HTTP) MCP endpoint follows the same shape, with a `url` instead of a `command`: ```json { "mcpServers": { "netexem": { "url": "https://api.netexem.com/v1/mcp", "headers": { "Authorization": "Bearer sk_live_…" } } } } ``` The API key carries full account access. Keep it in an environment variable or secret manager and never commit it to a shared repo. See [Authentication](/developers/docs/authentication). ## Tools it exposes The server registers a small set of tools, each mapped to a real API capability. Names are illustrative: | Tool | What it does | API equivalent | | --- | --- | --- | | `send_message` | Send an SMS/MMS from one of your numbers | `POST /messages` | | `find_contact` | Resolve a contact by phone or name | `GET /contacts/{id}` | | `create_contact` | Create a contact in the shared directory | `POST /contacts` | | `get_message` | Read a message's status and content | `GET /messages/{id}` | Phone numbers follow [E.164 format](/developers/glossary/e164), e.g. `+15551234567`. A `send_message` call looks like this from the agent's perspective: ```json { "name": "send_message", "arguments": { "from": "+15551234567", "to": "+15557654321", "body": "Hello from the agent" } } ``` ## How it fits with the rest of the API The MCP server complements — it does not replace — the [REST API](/developers/docs/api-reference) and [Webhooks](/developers/docs/webhooks): - **REST** is for your own backend code and scheduled jobs. - **MCP** is for AI agents acting interactively in a loop. - **Webhooks** push inbound messages and delivery receipts to your endpoint, each [HMAC-SHA256 signed](/developers/docs/webhooks), so your system can react to activity an agent didn't initiate. A common pattern: an agent uses MCP to send a text, the recipient replies, and a `message.received` webhook drives the follow-up — keeping humans, agents, and your backend on the same conversation. ## Next steps - [MCP for AI workflows](/developers/mcp) — what the MCP server unlocks across your stack - [Webhooks](/developers/docs/webhooks) — react to inbound activity in real time - [Authentication](/developers/docs/authentication) — keys, scopes and rotation --- Source: https://vancetel.com/developers/docs/changelog Markdown: https://vancetel.com/developers/docs/changelog.md # Changelog Changes to the VanceTel API are listed here in reverse-chronological order. Additive changes ship continuously; any breaking change is announced here ahead of time and versioned under a new base path. Want updates pushed to you instead? Inbound activity is delivered via signed [Webhooks](/developers/docs/webhooks). API release notes are published on this page. ## 2026-05-31 - Initial public API: messages, contacts, webhooks, deep links, and the MCP server. - `POST /messages` and `GET /messages/{id}` for sending and looking up SMS/MMS. See [Send an SMS](/developers/docs/sms). - `POST /contacts` and `GET /contacts/{id}` for creating and resolving contacts. See [Contacts](/developers/docs/contacts). - `message.received` webhook for inbound messages, [HMAC-SHA256 signed](/developers/docs/webhooks) via the `X-Netexem-Signature` header. - [Deep links](/developers/docs/deep-links) for click-to-call and pre-addressed texts (recipient pre-filled). - [MCP server](/developers/docs/mcp) so AI agents and Claude Code can act on the phone system in the loop. - Bearer [API key authentication](/developers/docs/authentication) on all endpoints.