Phone, SMS & voice
Send SMS and MMS from your numbers, handle replies and delivery receipts, respect opt-outs, look up line types and send one-time codes.
Send a text from any SMS-enabled number you own with one request. Replies land in the same inbox as your other channels, and delivery receipts arrive as webhooks. Outside the US a number can send as soon as SMS is on. In the US it must pass 10DLC registration first.
#Turn SMS on for a number
Not every number can text. Buy with wantsSms: true (or search with sms=true) to get one that can, then enable SMS on it:
curl -X POST https://api.pozy.ai/v1/phone-numbers/6650d2e1b3c4d5e6f7081923/sms \
-H "Authorization: Bearer $POZY_API_KEY"{
"enabled": true,
"id": "6650e3f2c4d5e6f708192a34",
"phoneNumber": "+14155550142",
"isActive": true,
"country": "US",
"smsCapable": true,
"mmsCapable": true,
"needsRegistration": true,
"alreadyRegistered": false,
"registrationStatus": null,
"reusable": null
}| Field | Meaning |
|---|---|
isActive |
SMS is on. The number receives texts right away. |
needsRegistration |
A US number that cannot send until a 10DLC or toll-free registration is approved |
reusable |
You already hold an approved registration this number can join for free |
smsCapable: false with enabled: false |
This number cannot text. Buy an SMS-capable one. |
notReady: true |
Still provisioning at the carrier. Retry in a minute. |
DELETE /v1/phone-numbers/{id}/sms turns SMS off again.
#Send a text
POST /v1/sms/messages takes from, to and text. Both numbers are normalized to E.164, so formatting does not matter.
curl -X POST https://api.pozy.ai/v1/sms/messages \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Idempotency-Key: order-1234-shipped" \
-H "Content-Type: application/json" \
-d '{
"from": "+14155550142",
"to": "+13125550188",
"text": "Acme: order #1234 has shipped. Reply STOP to opt out."
}'const res = await fetch("https://api.pozy.ai/v1/sms/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Idempotency-Key": "order-1234-shipped",
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "+14155550142",
to: "+13125550188",
text: "Acme: order #1234 has shipped. Reply STOP to opt out.",
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.id, data.conversationId, data.status);import os, requests
resp = requests.post(
"https://api.pozy.ai/v1/sms/messages",
headers={
"Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
"Idempotency-Key": "order-1234-shipped",
},
json={
"from": "+14155550142",
"to": "+13125550188",
"text": "Acme: order #1234 has shipped. Reply STOP to opt out.",
},
timeout=30,
)
print(resp.json()){ "id": "67c9d0e1f2a3b4c5d6e7f8a9", "conversationId": "66c3d2ae7b4f6c8d0e1f2a3b", "status": "sent" }sent means the carrier accepted the message. The final outcome comes by webhook. Send an Idempotency-Key so a retry returns the first response instead of texting twice (see Idempotency).
| Body field | What it does |
|---|---|
from |
One of your SMS-enabled numbers, or an alphanumeric sender ID |
to |
Recipient number |
text |
Message body, up to 10 segments. Optional when you send media. |
mediaUrls |
Up to 10 public URLs. Turns the message into an MMS. |
sendAt |
ISO 8601 time with offset, in the future, to schedule the message |
#Segments and encoding
Carriers bill and deliver SMS in segments. The encoding decides how many characters fit in each one.
| Encoding | Used when | One segment | Per segment of a longer message |
|---|---|---|---|
| GSM-7 | Plain Latin letters, digits and common symbols | 160 characters | 153 characters |
| UCS-2 | Any emoji, accented letters outside GSM-7, non-Latin scripts | 70 characters | 67 characters |
One emoji switches the whole message to UCS-2. A message is capped at 10 segments: 1,530 GSM-7 characters or 670 UCS-2 characters. Prices are per segment, by destination, and the carrier rates each message after it is sent, so there is no price quote endpoint for SMS.
#Send MMS
Add mediaUrls to attach images or video. text becomes the caption, or leave it out to send media only.
curl -X POST https://api.pozy.ai/v1/sms/messages \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+14155550142",
"to": "+13125550188",
"text": "Here is your receipt",
"mediaUrls": ["https://acme.example.com/receipts/1234.png"]
}'MMS is not available from sender IDs. To send the same message to many people, use an SMS broadcast instead of looping over this endpoint.
#Replies and delivery
| What happens | Event |
|---|---|
| The carrier delivered your message | message.delivered |
| Delivery failed (carrier error code included) | message.failed |
| Someone texts your number | message.received with platform: "sms" |
| The first message of a new thread | conversation.started |
Replies thread into the same inbox conversation as the message you sent (conversationId). Answer them with the inbox endpoints or another POST /v1/sms/messages. See Messages and Webhooks.
#Opt-outs
When a recipient replies STOP, the carrier opts them out. Further sends to them return 409: nothing is dropped silently. Only the recipient can opt back in, by replying START.
Keep your own suppression list in sync with GET /v1/sms/opt-outs:
curl "https://api.pozy.ai/v1/sms/opt-outs?limit=1000" \
-H "Authorization: Bearer $POZY_API_KEY"{
"optOuts": [
{ "phoneNumber": "+13125550199", "optedOutAt": "2026-10-28T09:30:00Z", "keyword": "STOP", "from": "+14155550142" }
],
"count": 1
}The list is most recent first. limit defaults to 500 and goes up to 5,000. There is no cursor, so raise limit instead of paging. Add format=csv for a file.
Put "Reply STOP to opt out" in the first message you send to a new contact. US carriers expect it, and it is one of the things 10DLC reviewers check.
#Look up a number
GET /v1/sms/lookup?number=%2B13125550188 returns the carrier, the line type (mobile, landline, voip, toll-free or unknown) and smsReachable. Each lookup is billed by the carrier-data source, so run it when you clean an opt-in list, not before every send.
#Send one-time codes
The Verify API is not available yet. Its endpoints answer 403 with code: "feature_not_available". Until it opens, send your own codes with POST /v1/sms/messages and check them on your side.
The Verify API sends a one-time code by SMS or WhatsApp and checks it for you. You never store the code.
curl -X POST https://api.pozy.ai/v1/verify/verifications \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "channel": "sms", "to": "+13125550188", "brandName": "Acme", "codeLength": 6, "ttlMinutes": 10 }'const vRes = await fetch("https://api.pozy.ai/v1/verify/verifications", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ channel: "sms", to: "+13125550188", brandName: "Acme" }),
});
const v = await vRes.json();
if (!vRes.ok) throw new Error(v.error);
// later, with the code the user typed
const resultRes = await fetch(`https://api.pozy.ai/v1/verify/verifications/${v.id}/check`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ code: "482913" }),
});
const result = await resultRes.json();
if (!resultRes.ok) throw new Error(result.error);
console.log(result.valid, result.status);v = requests.post(
"https://api.pozy.ai/v1/verify/verifications",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={"channel": "sms", "to": "+13125550188", "brandName": "Acme"},
timeout=30,
).json()
check = requests.post(
f"https://api.pozy.ai/v1/verify/verifications/{v['id']}/check",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={"code": "482913"},
timeout=30,
).json()
print(check["valid"], check["status"])| Field | Default | Notes |
|---|---|---|
channel |
required | sms or whatsapp |
codeLength |
6 | Up to 8 digits |
ttlMinutes |
10 | Up to 15 minutes |
brandName |
none | Up to 30 characters, shown in the message |
from |
auto | A number of yours to send from |
A verification moves through pending, then approved, expired, max_attempts_reached, canceled or delivery_failed. verification.approved fires when the right code is checked, and verification.failed (with reason: "max_attempts_reached") when attempts run out.
#Limits
| Limit | Value |
|---|---|
| Text length | 10 segments (1,530 GSM-7 or 670 UCS-2 characters) |
| MMS media | 10 public URLs per message |
| Destinations | Only countries on the SMS rate card. Others are blocked at send time, not billed. |
| Opt-out list | 500 by default, 5,000 at most, no cursor |
#Errors
| Status | Cause | Fix |
|---|---|---|
403 |
A US number whose registration is not approved yet | Wait for approved on the registration |
404 |
No SMS-enabled number you own matches from |
Send from one of your numbers |
409 |
The recipient replied STOP, or the same Idempotency-Key is still in flight |
Drop the recipient, or wait for the first request |
422 |
The Idempotency-Key was reused with a different body |
Use a new key for a new message |
402 |
No payment method on file | Add a payment method |
#Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/phone-numbers/{id}/sms |
Enable SMS on a number |
DELETE /v1/phone-numbers/{id}/sms |
Disable SMS on a number |
POST /v1/sms/messages |
Send an SMS/MMS |
GET /v1/sms/opt-outs |
List SMS opt-outs |
GET /v1/sms/lookup |
Look up carrier + line type |
POST /v1/verify/verifications |
Send a verification code (not available yet) |
GET /v1/verify/verifications/{verificationId} |
Get a verification (not available yet) |
POST /v1/verify/verifications/{verificationId}/check |
Check a verification code (not available yet) |
#Webhook events
| Event | When it fires |
|---|---|
message.received |
Message received. |
conversation.started |
Conversation started. |
message.delivered |
Message delivered. |
message.failed |
Message delivery failed. |
verification.approved |
Verification approved. |
verification.failed |
Verification failed. |