Recipes
Send a text
Written beside the API’s code, and read from it every hour.
Send from a texting number on your account to a US or Canadian number. Try a dry run first: with "test": true the API checks everything and sends nothing. A dry run is never sent or billed, but it counts as a request toward the rate limits.
# 1. Dry run: checks everything, sends nothing
curl -X POST https://api.vocatech.com/v1/messages \
-H "Authorization: Bearer $VOCATECH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "text",
"from": "7185550100",
"to": "7185550142",
"message": "Hi Jordan, your appointment is confirmed for Friday at 9 AM.",
"test": true
}'
# 2. Send it: the same body without "test"
curl -X POST https://api.vocatech.com/v1/messages \
-H "Authorization: Bearer $VOCATECH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "text",
"from": "7185550100",
"to": "7185550142",
"message": "Hi Jordan, your appointment is confirmed for Friday at 9 AM."
}'import { vt } from './vocatech.mjs';
const text = {
platform: 'text',
from: '7185550100', // a texting number on your account
to: '7185550142',
message: 'Hi Jordan, your appointment is confirmed for Friday at 9 AM.',
};
// 1. Dry run: checks everything, sends nothing
const check = await vt('POST', '/v1/messages', { ...text, test: true });
console.log('valid:', check.message.valid, 'parts:', check.message.messages_counted);
// 2. Send it
try {
const res = await vt('POST', '/v1/messages', text);
if (res.status === 'queued') {
console.log('queued at position', res.message.position); // 202, API Messaging keys
} else if (res.message.message_sent) {
console.log('sent:', res.status); // 201, or 200 into an existing Webex space
} else {
console.warn('taken but not sent:', res.message);
}
} catch (err) {
if (err.status === 503) console.warn('Not sent. Safe to retry.');
else if (err.status === 504) console.warn('May have gone out. Check GET /v1/messages first.');
else throw err;
}from vocatech import VocatechError, vt
text = {
"platform": "text",
"from": "7185550100", # a texting number on your account
"to": "7185550142",
"message": "Hi Jordan, your appointment is confirmed for Friday at 9 AM.",
}
# 1. Dry run: checks everything, sends nothing
check = vt("POST", "/v1/messages", {**text, "test": True})
print("valid:", check["message"]["valid"], "parts:", check["message"]["messages_counted"])
# 2. Send it
try:
res = vt("POST", "/v1/messages", text)
if res["status"] == "queued":
print("queued at position", res["message"]["position"]) # 202, API Messaging keys
elif res["message"].get("message_sent"):
print("sent:", res["status"]) # 201, or 200 into an existing Webex space
else:
print("taken but not sent:", res["message"])
except VocatechError as err:
if err.status == 503:
print("Not sent. Safe to retry.")
elif err.status == 504:
print("May have gone out. Check GET /v1/messages first.")
else:
raise<?php
require __DIR__ . '/vocatech.php';
$text = [
'platform' => 'text',
'from' => '7185550100', // a texting number on your account
'to' => '7185550142',
'message' => 'Hi Jordan, your appointment is confirmed for Friday at 9 AM.',
];
// 1. Dry run: checks everything, sends nothing
$check = vt('POST', '/v1/messages', $text + ['test' => true]);
echo 'parts: ', $check['message']['messages_counted'], PHP_EOL;
// 2. Send it
try {
$res = vt('POST', '/v1/messages', $text);
if ($res['status'] === 'queued') {
echo 'queued at position ', $res['message']['position'], PHP_EOL; // 202
} elseif (!empty($res['message']['message_sent'])) {
echo 'sent: ', $res['status'], PHP_EOL; // 201, or 200 into an existing Webex space
} else {
echo "taken but not sent\n";
}
} catch (VocatechError $e) {
if ($e->status === 503) {
echo "Not sent. Safe to retry.\n";
} elseif ($e->status === 504) {
echo "May have gone out. Check GET /v1/messages first.\n";
} else {
throw $e;
}
}{
"status": "test",
"message": {
"mode": "api",
"platform": "text",
"from": "7185550100",
"to": "7185550142",
"name": "Vocatech Text",
"email_recipient": null,
"valid": true,
"messages_counted": 1
}
}{
"status": "created",
"message": {
"message_id": "msg_7f3a9c",
"mode": "api",
"platform": "text",
"from": "7185550100",
"to": "7185550142",
"name": "Vocatech Text",
"email_recipient": null,
"message_sent": true,
"email_sent": false,
"created_at": "2026-09-24T09:15:02-04:00",
"messages_counted": 1
}
}Check message.message_sent. A 201 with message_sent: false was taken but did not go out. messages_counted is the number of parts the text is paced and billed as.
What the status means
| Status | Means | Do this |
|---|---|---|
200 | A dry run passed (status: "test"), or a Webex number posted the text into the contact's existing conversation (status: "sent"). | After a dry run, send for real. Otherwise check message_sent. |
201 | Sent (status: "created"). | Check message_sent. |
202 | Queued (status: "queued"). API Messaging keys only: it goes out in order at the key's pace. | Track it with GET /v1/messages/queue. |
400 | A field is wrong: a number, the length, an attachment link. | Fix it. The message names it. |
403 | Refused: from is not a texting number on your account, the recipient opted out, or the text is over the carrier's limit. | Do not retry as is. |
422 | WhatsApp sending through the API is not available. | Send WhatsApp from Webex or the portal. |
429 | A limit was reached. | Wait Retry-After seconds. |
502 | The messaging service answered and did not send. | Read the message. Retry later if it is temporary. |
503 | The messaging service could not be reached. Nothing was sent. | Safe to retry. |
504 | The send was not confirmed. It may have gone out. | Check GET /v1/messages before you retry. |
Pictures and files (MMS)
Add media: up to 10 https links to files your server hosts on a public address. Every file goes out in one MMS, and an MMS counts as one message however long its text. Each link must end in one of these: jpg, jpeg, png, gif, bmp, webp, mp4, 3gp, mov, mp3, wav, amr, ogg, pdf, vcf or txt. Carriers cap MMS size, often between 500 KB and 1 MB, so keep files small.
curl -X POST https://api.vocatech.com/v1/messages \
-H "Authorization: Bearer $VOCATECH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "text",
"from": "7185550100",
"to": "7185550142",
"message": "Here is the map to our new office.",
"media": ["https://YOUR_DOMAIN/files/office-map.png"]
}'import { vt } from './vocatech.mjs';
const res = await vt('POST', '/v1/messages', {
platform: 'text',
from: '7185550100',
to: '7185550142',
message: 'Here is the map to our new office.',
media: ['https://YOUR_DOMAIN/files/office-map.png'], // up to 10 links
});
console.log(res.message.mms, res.message.attachments_sent);from vocatech import vt
res = vt("POST", "/v1/messages", {
"platform": "text",
"from": "7185550100",
"to": "7185550142",
"message": "Here is the map to our new office.",
"media": ["https://YOUR_DOMAIN/files/office-map.png"], # up to 10 links
})
print(res["message"]["mms"], res["message"]["attachments_sent"])<?php
require __DIR__ . '/vocatech.php';
$res = vt('POST', '/v1/messages', [
'platform' => 'text',
'from' => '7185550100',
'to' => '7185550142',
'message' => 'Here is the map to our new office.',
'media' => ['https://YOUR_DOMAIN/files/office-map.png'], // up to 10 links
]);
echo $res['message']['attachments_sent'], " attachment(s) sent\n";{
"status": "created",
"message": {
"message_id": "msg_7f3a9d",
"mode": "api",
"platform": "text",
"from": "7185550100",
"to": "7185550142",
"mms": true,
"attachments_sent": 1,
"mirrored_to_webex": false,
"message_sent": true,
"created_at": "2026-09-24T09:18:40-04:00",
"messages_counted": 1
}
}Long texts
A text can run to 1,600 characters. Carriers split it into parts: 160 characters fit in one part, and 153 fit in each part once it splits. One character outside the basic set, such as an emoji or a curly quote, moves the whole text to 70 per part, 67 once it splits. messages_counted says how many parts you sent.
Delivery receipts
A 201 means the text left us. Whether the phone got it arrives later as a message.status_updated webhook. GET /v1/messages lists texts as delivered and does not carry carrier receipts.
Sending WhatsApp through the API is not available. "platform": "whatsapp" answers 422, dry runs included. WhatsApp history is in GET /v1/messages, and replies go out from Webex or the portal.
{
"error": {
"code": 422,
"type": "UNPROCESSABLE_ENTITY",
"message": "Sending WhatsApp messages through the API is not available yet. WhatsApp history is in GET /v1/messages; reply from Webex or the portal."
}
}