Documentation
Comms
comms.send_message
REST
ShippingPOST /api/v1/comms/send_messageMCP tool
Livecomms.send_messageExposed on: REST API · Shop MCP server · Claude connector · Dash (in-app copilot) · Zapier. Part of the Comms domain.
Operating contract
Sends ONE SMS, now, to a customer already on the shop's book. It lands in their inbox thread, so the shop sees it beside everything else and the customer's reply comes back to the same place.
THE RECIPIENT IS A CUSTOMER ID, NEVER A RAW NUMBER, and that is not a convenience — it is what stops this being a spam cannon borrowing the shop's A2P registration. If you have a phone number and not a customer, comms.resolve_contact finds the record; if there is genuinely no record, customers.create makes one, and the shop's own consent rules then apply to it.
FOUR THINGS CAN REFUSE IT, all re-checked at the moment of sending rather than when you read the thread: the shop's messaging master switch and sender number, the customer's texting block (a STOP or a staff block), the shop's quiet hours, and the plan. Each refusal names itself. NONE of them is transient — retrying a quiet-hours refusal in a loop will fail every time until the window opens, and retrying an opt-out refusal is a compliance problem, not a retry.
LINKS ARE STRIPPED, NOT REFUSED, ON THIS PATH. While the platform's link guardrail is on, any URL in the body is removed before the text goes out and before the row is written, and a short pointer is appended telling the customer to reply. So do not put a link in a text and expect it to arrive: if the link IS the message, send it by email instead (comms.send_email), and say in the text that it has been emailed only if it actually has.
IT IS NOT IDEMPOTENT. A repeat is a second text on somebody's phone and no key can make it not. Read comms.list_messages on the thread before re-sending anything you are unsure about.
Who may call it
- Permission
inbox.accessThe caller must hold Inbox at the ACT level. A read-only dashboard grant on the same section is refused.- Plan
- Every planNo plan gate. Available on every Service VIN plan.
- Retries
noneA repeat genuinely does it twice and no key can make it not. Read before you write; the operating contract says what to read.- Rate class
sendCounted against the send budget, the tightest one: what a runaway loop spends here is the shop's own sending reputation.
Input
| Field | Type | Description |
|---|---|---|
| customer_idrequired | stringuuid | Who to text, as returned by customers.list, customers.find_by_contact or comms.resolve_contact. |
| bodyrequired | stringmax 1600 chars, min 1 chars | The text to send, 1-1600 characters. Any URL in it will be removed before sending. |
| conversation | stringuuid | Land it in this thread, as returned by comms.list_conversations. Omit to use the customer's open thread. |
Output
| Field | Type | Description |
|---|---|---|
| id | string | null | — |
| conversation | string | — |
| customer_id | string | — |
| body | string | — |
| to | string | — |
| status | string | — |
| created_at | string | — |
Examples
Built from this capability's own schema — required fields and the ones carrying a default, and nothing invented. Paste one and it validates.
export SERVICEVIN_API_KEY=svk_live_…
curl -X POST https://www.servicevin.com/api/v1/comms/send_message \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"customer_id":"9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20","body":"Left a voicemail about Friday."}'const res = await fetch("https://www.servicevin.com/api/v1/comms/send_message", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"customer_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
"body": "Left a voicemail about Friday."
}),
});
// Success and failure are both envelopes. Switch on error.code, never
// on error.message — the codes are stable, the messages are for people.
const payload = await res.json();
if (!res.ok) throw new Error(payload.error.code);
const data = payload.data;import os, requests
res = requests.post(
"https://www.servicevin.com/api/v1/comms/send_message",
headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
json={
"customer_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
"body": "Left a voicemail about Friday."
},
timeout=30,
)
payload = res.json()
if not res.ok:
raise RuntimeError(payload["error"]["code"])
data = payload["data"]{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "comms.send_message",
"arguments": {
"customer_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
"body": "Left a voicemail about Friday."
}
}
}Refusals
The four gates run in this order on every surface, and the order is not arbitrary — see Authentication.
| Status | Code | When |
|---|---|---|
| 404 | not_found | The id is unknown, or the feature is not enabled for this account. Deliberately the same answer for both. |
| 403 | forbidden | This login does not hold inbox.access. |
| 403 | insufficient_scope | The credential is read-only and this capability writes. |
| 422 | validation_error | An argument was wrong. The message names the field. |
| 429 | rate_limited | Too many send calls. Back off and retry. |
| 500 | internal_error | Something failed on our side. Nothing was changed. |