Send a message
POST /v1/messages
| Property | Value |
|---|---|
| Operation | create_message |
| Group | Messages |
| Scope | messages:write |
| Route class | control |
| Idempotency | Idempotency-Key required: a retry with the same key replays the first answer |
| Audit | Audited channels.message.send (mutation) |
| Released in | 1.60.0 |
| Phase 0 host | Served (the hosted api pins 1.65.0) |
The send action (1.60.0; channels.md section 2): one message to one recipient over one Connection through one of its send tools. In 1.60.0 every request is refused with capability_not_available and nothing is sent. From 1.61.0 (roadmap slice 2.15) the route checks the recipient's consent on the tool's channel for the Organization and for the Connection as sender, the rule of check_consent, and refuses an opted-out recipient with 422 contact_opted_out, one detail whose reason is a ConsentCheckReason, queuing nothing; otherwise it records the Message queued and hands it to the exec role as a leased action task, which checks the consent again before it sends (check_send_consent). Quiet hours defer a Message (deferred), never drop it. The Audit event names the Connection and the tool, never the recipient or the text.
Request
Set VOCAL_API to the base URL of your API and VOCAL_API_KEY to an API key with the messages:write scope (Authentication and keys). Replace each placeholder, such as agt_... or <name>, with your own value.
curl -X POST "$VOCAL_API/v1/messages" \
-H "Authorization: Bearer $VOCAL_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"connection_id": "con_...",
"tool": "<tool>",
"to": "<to>",
"body": "<body>"
}'const response = await fetch(`${process.env.VOCAL_API}/v1/messages`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCAL_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
"connection_id": "con_...",
"tool": "<tool>",
"to": "<to>",
"body": "<body>"
}),
});
console.log(response.status, await response.text());import os
import uuid
import requests
response = requests.post(
f"{os.environ['VOCAL_API']}/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"connection_id": "con_...",
"tool": "<tool>",
"to": "<to>",
"body": "<body>",
},
)
print(response.status_code, response.text)Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
idempotency-key | header | string | yes | A client-chosen key, kept 24 hours per Organization, environment and route. A replay returns the stored response. |
Request body
Content type application/json, required: MessageCreateRequest.
| Field | Type | Required | Description |
|---|---|---|---|
body | string | yes | |
connection_id | ConnectionId | yes | |
related_call_id | CallId | no | |
subject | string | no | |
to | string | yes | |
tool | UpstreamToolName | yes | A send tool of the Connection's Connector. |
Responses
| Status | Content type | Shape | Error codes |
|---|---|---|---|
201 | application/json | Message | |
400 | application/json | ErrorEnvelope | invalid_request |
401 | application/json | ErrorEnvelope | unauthenticated |
403 | application/json | ErrorEnvelope | forbidden, org_suspended |
409 | application/json | ErrorEnvelope | connection_revoked, idempotency_in_progress, idempotency_key_reused |
422 | application/json | ErrorEnvelope | validation_failed, capability_not_available, contact_opted_out |
429 | application/json | ErrorEnvelope | rate_limited |
500 | application/json | ErrorEnvelope | internal_error |
Fields of the 201 answer
The shape is Message.
| Field | Type | Required | Description |
|---|---|---|---|
action | MessagingAction | yes | |
channel | MessageChannel | yes | |
connection_id | ConnectionId | yes | |
content | MessageContent or null | yes | The recipient and text: answered by the single read, an audited content read; null in a list. |
created_at | Timestamp | yes | |
deferred_until | Timestamp or null | yes | When a deferred Message is sent; null otherwise. |
delivered_at | Timestamp or null | yes | |
direction | MessageDirection | yes | |
environment | Environment | yes | |
error_code | ToolErrorCode or null | yes | |
id | MessageId | yes | |
notification_rule_id | NotificationRuleId or null | yes | |
object | "message" | yes | |
organization_id | OrganizationId | yes | |
related_call_id | CallId or null | yes | |
run_id | RunId or null | yes | |
segments | integer or null | yes | The SMS segments counted; null for other channels. |
sent_at | Timestamp or null | yes | |
status | MessageStatus | yes | |
thread_id | null | yes | Reserved for conversation Channels; always null. |
tool | UpstreamToolName | yes | |
trigger_event_id | TriggerEventId or null | yes | |
updated_at | Timestamp | yes | |
vendor_message_id | string or null | yes |