Hanzo AI

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

CapabilityWhat it doesOperations
/v1/telNumbers the org holds, and the calls and messages over them10
/v1/channelsChat on Discord, Slack, Teams and Telegram7
/v1/notifyTransactional email and SMS through the org's own provider credential4

Nouns

TwilioHanzo
Account SID and auth tokenOne key. Authorization: Bearer $HANZO_API_KEY
POST /2010-04-01/Accounts/{sid}/Messages.jsonPOST /v1/tel/messagesfrom, to, text, media
POST .../Calls.jsonPOST /v1/tel/callsfrom, to, agent, record, webhook
GET .../AvailablePhoneNumbers/{Country}/Local.jsonGET /v1/tel/numbers/available?Country=&Area=&Type=&Limit=
POST .../IncomingPhoneNumbers.jsonPOST /v1/tel/numberse164
Releasing a numberDELETE /v1/tel/numbers/{id}
Message and call logsGET /v1/tel/messages · GET /v1/tel/calls
Hanging up a live callDELETE /v1/tel/calls/{id}
TwiML answering a callagent — the Hanzo assistant that answers it
Twilio Email or SendGridPOST /v1/notify/send/email?sync=true
A transactional SMS through your own Twilio accountPOST /v1/notify/send/sms?sync=true
Conversations on chat transportsPOST /v1/channels/{channel}/senddiscord, 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?