Twilio
Twilio holds phone numbers and moves messages over them. Three capabilities answer it — /v1/tel for the carrier, /v1/notify for transactional delivery, /v1/channels for chat.
Twilio is three products wearing one account: numbers and the calls and texts
over them, transactional delivery through a provider you configure, and chat on
somebody else's transport. Each has its own address here — /v1/tel (10
operations), /v1/notify (4), /v1/channels (7).
Start here
Two calls after the key: read the number the org holds, then send from it.
# 1. mint a key — sk- belongs on a server, pk- is safe in a browser
curl -sS -X POST https://api.hanzo.ai/v1/account/keys \
-H "Authorization: Bearer $HANZO_SESSION" \
-H 'Content-Type: application/json' \
-d '{"type":"secret"}'
# 2. what the org holds — this is where a legal `from` comes from
curl -sS https://api.hanzo.ai/v1/tel/numbers \
-H "Authorization: Bearer $HANZO_API_KEY"
# 3. send from one of them
curl -sS -X POST https://api.hanzo.ai/v1/tel/messages \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"from": "+14155550142", "to": "+15558675309", "text": "Hello from Hanzo!"}'Step 3 is refused unless from is a number step 2 returned, checked against our
own store rather than the carrier's word — so a 201 is evidence the org really
holds the sender. If step 2 is empty, search with GET /v1/tel/numbers/available
and buy with POST /v1/tel/numbers before sending.
Core capabilities
| Capability | What it does | Operations |
|---|---|---|
/v1/tel | Numbers the org holds, and the calls and messages over them | 10 |
/v1/channels | Chat on Discord, Slack, Teams and Telegram | 7 |
/v1/notify | Transactional email and SMS through the org's own provider credential | 4 |
Nouns
| Twilio | Hanzo |
|---|---|
| Account SID and auth token | One key. Authorization: Bearer $HANZO_API_KEY |
POST /2010-04-01/Accounts/{sid}/Messages.json | POST /v1/tel/messages — from, to, text, media |
POST .../Calls.json | POST /v1/tel/calls — from, to, agent, record, webhook |
GET .../AvailablePhoneNumbers/{Country}/Local.json | GET /v1/tel/numbers/available?Country=&Area=&Type=&Limit= |
POST .../IncomingPhoneNumbers.json | POST /v1/tel/numbers — e164 |
| Releasing a number | DELETE /v1/tel/numbers/{id} |
| Message and call logs | GET /v1/tel/messages · GET /v1/tel/calls |
| Hanging up a live call | DELETE /v1/tel/calls/{id} |
| TwiML answering a call | agent — the Hanzo assistant that answers it |
| Twilio Email or SendGrid | POST /v1/notify/send/email?sync=true |
| A transactional SMS through your own Twilio account | POST /v1/notify/send/sms?sync=true |
| Conversations on chat transports | POST /v1/channels/{channel}/send — discord, slack, teams, telegram |
/v1/tel is the carrier: numbers the org bought, and traffic over them.
/v1/notify is delivery through the org's own provider credential, read from
KMS — naming no provider picks the one that is actually configured (Twilio, then
Plivo for SMS; Twilio Email, then SMTP for email) and fails closed when none is.
If you are keeping your Twilio account and only moving the code, /v1/notify is
the shorter migration.
The call
Twilio:
curl -X POST "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Messages.json" \
-u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
--data-urlencode "From=+15017250604" \
--data-urlencode "To=+15558675309" \
--data-urlencode "Body=Hello from Twilio!"Hanzo, from a number the org holds:
curl -X POST https://api.hanzo.ai/v1/tel/messages \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"from": "+15017250604",
"to": "+15558675309",
"text": "Hello from Hanzo!"
}'Buying the number first:
# What the carrier has.
curl -sS "https://api.hanzo.ai/v1/tel/numbers/available?Country=US&Area=415&Limit=5" \
-H "Authorization: Bearer $HANZO_API_KEY"
# Take one. Provisioning happens with the carrier first and is recorded second,
# so a row here means a number the org really holds.
curl -sS -X POST https://api.hanzo.ai/v1/tel/numbers \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"e164": "+14155550142"}'Dialling, with an assistant to answer:
curl -X POST https://api.hanzo.ai/v1/tel/calls \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"from": "+15017250604", "to": "+15558675309", "agent": "support", "record": true}'The call is refused up front when no assistant plane is configured, because a call that connects to silence has already cost the person who answered it.
What does not carry
No TwiML. Twilio hands control of a live call to a markup document —
<Say>, <Gather>, <Dial> — fetched from your server. There is no markup
here. A call is answered by an agent, and webhook is where events go. An IVR
expressed as TwiML has to be re-expressed as an assistant.
from must be a number the org holds. Twilio will send from a Messaging
Service, an alphanumeric sender id, or a pool. Here the sender is one of the
org's own numbers and nothing else.
No status callbacks per message. The message body is from, to, text and
media — there is no StatusCallback field. Delivery state is the status on
the row, read back with GET /v1/tel/messages.
No WhatsApp and no RCS. /v1/channels covers Discord, Slack, Teams and
Telegram. All four render text today, so attachments and actions are flattened
to one line each after the text — never dropped.
No subaccounts, no Messaging Services, no regions or edges. Tenancy is the
org in the key. There is no account tree to mirror and no region/edge to
select.
No Verify. Twilio Verify mints a code, delivers it, and checks it back.
/v1/notify delivers a message you composed. Identity flows belong to
IAM, not to the sender.
sync=true is required on notify. An async dispatch answers 503 — the queue
plane that would run it is owned elsewhere. It rides as ?sync=true on the URL,
which binds over the body.
How is this guide?
Amazon SQS
SQS is a queue between services. Here that is /v1/mq for durable streams and consumers, and /v1/pubsub for the publish itself.
AgentMail
AgentMail gives an agent its own mailbox. Here /v1/notify sends and /v1/channels carries the conversation — but there is no mailbox, so read the last section first.