Sections
API Docs
SMS API
Send your first Canadian SMS in under 5 minutes.
Authentication, API keys, webhook setup and signatures, rate limits, the SDK, the MCP server and the general error codes are on the Platform page.
Quickstart
- Create a free account, then open API Keys → Create key in the dashboard. The full key is shown once, right after you create it, so copy it then.
- Start with a test key: it works immediately, before any top-up, so you can send test messages and explore the API for free.
- Top up your balance with a card via Stripe. Live keys return 402 PAYMENT_REQUIRED until the first top-up.
- Verify your phone number as the account owner. A live send needs a verified owner phone.
- Buy a Canadian number to send from; a live send needs a number your account owns.
- Record CASL consent for each phone number you will message.
- Send your first message using the REST API.
Phone Numbers
Search for available Canadian numbers, provision one, and use it as the from field when sending.
curl https://api.honkio.ca/v1/phone-numbers/search?area_codes=416 \
-H "Authorization: Bearer mk_live_YOUR_KEY"
# Response 200 (per result): what buying it charges now and monthly, in CAD cents
# { "phone_number": "+14165550100", "region": "Ontario",
# "upfront_cost_cents": 250, "activation_fee_cents": 100, "monthly_cost_cents": 250, ... }
# Provision a number
curl -X POST https://api.honkio.ca/v1/phone-numbers \
-H "Authorization: Bearer mk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"phone_number": "+14165550100"}'
# Accounts hold a limited number of numbers. GET /v1/accounts/me reports
# phone_number_limit and phone_numbers_used: check them before buying, or
# handle the 403 NUMBER_LIMIT_REACHED that a purchase past the cap returns.
# Ask HonkIO staff to raise the limit. One request may be pending at a time.
curl -X POST https://api.honkio.ca/v1/phone-numbers/allowance-requests \
-H "Authorization: Bearer mk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"requested_limit": 10, "reason": "Onboarding three new clinics this quarter"}'
# Response 201: { "status": "PENDING", "requested_limit": 10, ... }
# You are emailed if it is approved; the decision also shows in the dashboard.
# Check on it
curl https://api.honkio.ca/v1/phone-numbers/allowance-requests \
-H "Authorization: Bearer mk_live_YOUR_KEY"CASL Consent (required before sending)
Under CASL, you must record consent before sending a commercial message to any recipient. The API will block sends to phone numbers without valid consent (HTTP 451).
curl -X POST https://api.honkio.ca/v1/compliance/consents \
-H "Authorization: Bearer mk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+1613XXXXXXX",
"consent_type": "express",
"source_description": "Website opt-in form",
"source_ip": "203.0.113.1"
}'Implied consent for an existing customer, with the clock running from their last transaction:
curl -X POST https://api.honkio.ca/v1/compliance/consents \
-H "Authorization: Bearer mk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+1613XXXXXXX",
"consent_type": "implied",
"relationship_type": "purchase",
"last_transaction_date": "2025-11-04"
}'
# → { "status": "recorded", "phone_number": "+1613XXXXXXX", "expires_at": "2027-11-04T00:00:00.000Z" }Express consent never expires. Implied consent expires after 2 years per CASL §10(9). Pass last_transaction_date so the two-year clock runs from the real relationship rather than from the day you record it, or set expires_at outright when you have already worked out the expiry. The response echoes expires_at so you can verify it.
Sending SMS
Send a message using a provisioned number. The API validates the Canadian destination number, and checks CASL consent before delivery (CRTC DNCL checking coming soon).
# "from" is one of your HonkIO numbers; "to" is a real number you hold consent for
curl -X POST https://api.honkio.ca/v1/messages \
-H "Authorization: Bearer mk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+1416XXXXXXX",
"to": "+1613XXXXXXX",
"body": "Hello from HonkIO! 🇨🇦"
}'Phone Number Verification (OTP)
Use the Verify API to confirm ownership of a phone number before sending commercial messages. Your end-user receives a one-time code via SMS; submit it to the check endpoint to confirm.
# Start a verification (sends OTP SMS)
curl -X POST https://api.honkio.ca/v1/verify -H "Authorization: Bearer mk_live_YOUR_KEY" -H "Content-Type: application/json" -d '{
"from": "+1416XXXXXXX",
"to": "+1613XXXXXXX",
"code_length": 6,
"ttl_minutes": 10,
"app_name": "Acme"
}'
# Response: { "id": "clxxx...", "status": "pending", "code_length": 6, ... }
# Check the code submitted by your user
curl -X POST https://api.honkio.ca/v1/verify/clxxx.../check -H "Authorization: Bearer mk_live_YOUR_KEY" -H "Content-Type: application/json" -d '{ "code": "483721" }'
# Response 200: { "status": "verified", ... }
# Response 422: { "code": "VERIFICATION_INVALID_CODE", "attempts_remaining": 4 }
# Fetch status at any time
curl https://api.honkio.ca/v1/verify/clxxx... -H "Authorization: Bearer mk_live_YOUR_KEY"In test mode the code is always all zeros for the chosen length (e.g. 000000 for 6-digit). No SMS is sent and nothing is billed. Every verification returns a "mode" field of "LIVE" or "TEST" so you can tell a simulated verification from a real one.
Pricing
Prices are set at runtime and can change without a release, so read them rather than hardcoding them. All amounts are CAD cents. Sending is billed per SMS part: message_cost_cents × the parts the carrier splits the body into. Parts are counted the way the carrier counts them: typographic quotes, dashes and ellipses are smart-encoded to GSM-7 (160 characters, then 153 per part), while emoji and most accented letters force Unicode parts (70, then 67). The charge is settled to the carrier's part count after the send. A message the carrier rejects outright costs nothing, and so does a send to a reserved exchange (555-XXXX and similar), which is refused here; a message the carrier accepts but cannot deliver keeps its charge. A body over 10 parts is refused with 422 MESSAGE_TOO_LONG before any charge. verification_cost_cents covers a typical single-part OTP; a long or non-GSM app_name can add a part. phone_number_activation_fee_cents is charged once, together with the first month, on every number provisioned, local or toll-free, and is not refunded on release. inbound_message_cost_cents is charged per part of every SMS received on a provisioned number, sender and carrier included, except STOP, START and HELP keywords; a received message is debited even if it takes the balance below zero, which pauses sending until the next top-up.
# Current prices, in CAD cents
curl https://api.honkio.ca/v1/pricing \
-H "Authorization: Bearer mk_live_YOUR_KEY"
# Response 200:
# {
# "message_cost_cents": 3,
# "verification_upcharge_cents": 25,
# "verification_cost_cents": 28,
# "phone_number_upfront_cost_cents": 250,
# "phone_number_monthly_cost_cents": 250,
# "phone_number_activation_fee_cents": 100,
# "inbound_message_cost_cents": 3
# }Test-mode requests are priced identically in the response but never charged, so you can see what an integration would cost before spending anything.
Sending limits
HonkIO is built for transactional and relationship messaging, not campaigns, and every customer’s deliverability rides on a shared carrier profile. These limits keep bulk marketing off the platform; a clinic, a contractor or a SaaS sending codes will not notice them. The sending limits apply to live mode only; the link-shortener rule, the reserved and undeliverable destination checks, and the broadcast group-size cap run in both modes.
- Daily cap: new accounts can send 250 live messages per rolling 24 hours. It does not lift on its own. 30 days after your first live message you can request a higher volume from the dashboard; approval sets 1,000 a day or the figure you asked for. Refused sends return 429 DAILY_LIMIT_REACHED with your limit and count.
- Identical messages: the same body may reach at most 250 distinct recipients per 24 hours (429 FANOUT_LIMIT_REACHED). Personalized messages are unaffected.
- Per-number rate: 60 messages per minute per sending number, which is what Canadian carriers grant a long code anyway (429 NUMBER_RATE_LIMITED with Retry-After).
- Broadcasts: up to 250 recipients per contact-group broadcast and 3 broadcasts per 24 hours (422 BROADCAST_TOO_LARGE, 429 BROADCAST_LIMIT_REACHED).
- Link shorteners (bit.ly, tinyurl and similar) are refused in both modes because carriers filter them (422 LINK_SHORTENER_BLOCKED). Use the full URL.
- Delivery warning, then automatic pause: if more than 10% of your last 50 live messages fail at the carrier, you are emailed (and account.delivery_warning fires) without any pause. If more than 1% of recipients reply STOP, or more than 5% of messages are rejected by carriers, over your recent sends, live sending pauses for 24 hours and you are emailed (403 SENDING_PAUSED with the resume time; account.sending_paused fires).
- Top-ups: the balance cannot exceed $500 and top-ups are limited to $1,000 per 30 days. Raised on request.
- Per-recipient cap: 30 messages to one recipient per hour and 100 per 24 hours (429 RECIPIENT_RATE_LIMITED with Retry-After). A two-way conversation never gets near it; a script retrying one number does.
- Undelivered messages are billed: a message the carrier accepts but cannot deliver keeps its charge. A reserved exchange (555-XXXX, N11 exchanges such as 411 or 911, carrier test codes, exchanges starting with 0 or 1) is refused for free with 422 RESERVED_DESTINATION, in test mode too, and a reserved area code (555, 911 and the like) as non-Canadian. Anything that retries should send an Idempotency-Key header so a retry can never become a second charge. An hour of unusual spend triggers an email and account.spend_warning.
- Undeliverable numbers: after 3 consecutive carrier failures to one number within 30 days, from any HonkIO customer, sends to it are refused without charge (422 UNDELIVERABLE_NUMBER with the failure count, when it was listed and when it expires) for 90 days, then retried in case the number was reassigned.
curl https://api.honkio.ca/v1/send-limit \
-H "Authorization: Bearer mk_live_YOUR_KEY"
# → { "daily_limit": 250, "sent_last_24h": 12, "remaining": 238,
# "recipient_rate_per_hour": 30, "recipient_rate_per_day": 100,
# "probation": { "ends_at": "2026-09-26T14:02:11.000Z", "eligible_to_request": false },
# "paused_until": null, "requests": [] }Read your current limits and usage with GET /v1/send-limit, and file a request with POST /v1/send-limit/requests. Every figure above is a platform default that can be raised per account.
Webhook events
These are the SMS, opt-out and phone number events. Registering an endpoint, the envelope every event arrives in, retries and signatures are covered under Platform webhooks.
Every payload is signed with HMAC-SHA256, so verify the X-HonkIO-Signature header. A message.received event carries the sender, your number, the body, keyword_action (STOP/START handling), and the segment_count and cost_cents the message was billed at. A message.sent event carries carrier_message_id, the id the carrier assigned to the message, with the same two billing fields.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "message.delivered",
"created": "2026-09-19T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"message_id": "clxxxmessagexxxxxxxxxxxxx",
"status": "delivered",
"message_status": "DELIVERED",
"to": "+1613XXXXXXX",
"from": "+1416XXXXXXX",
"errors": []
}
}
// Headers: X-HonkIO-Signature, X-HonkIO-Timestamp, X-HonkIO-EventA message id can emit message.failed (only when the failure was CARRIER_UNAVAILABLE; an INSUFFICIENT_BALANCE failure fires no webhook) and later message.queued again, then message.sent, when a retry with the same Idempotency-Key re-attempts a message that failed before it reached the carrier.
Every event, as your endpoint receives it. The examples are redacted: ids, numbers and addresses are placeholders. Fields can be added over time, so ignore any you do not recognise.
message.queued
An outbound message was accepted and charged, and is about to be handed to the carrier.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "message.queued",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"message_id": "clxxxmessagexxxxxxxxxxxxx",
"from": "+1416XXXXXXX",
"to": "+1613XXXXXXX",
"status": "QUEUED",
"message_status": "QUEUED"
}
}| data field | Meaning |
|---|---|
| message_id | The HonkIO message id, as returned by POST /v1/messages or listed by GET /v1/messages. |
| from | Your HonkIO number the message was sent from, E.164. |
| to | The recipient's phone number, E.164. |
| status | The message status at this point, upper case (the same value as message_status). |
| message_status | HonkIO's status for the message, upper case: QUEUED, SENDING, DELIVERED, FAILED, UNDELIVERED or RECEIVED. |
message.sending
Reserved: rarely if ever sent. It fires only when a final carrier receipt reports the sending status, which is not expected in practice.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "message.sending",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"message_id": "clxxxmessagexxxxxxxxxxxxx",
"status": "sending",
"message_status": "SENDING",
"to": "+1613XXXXXXX",
"from": "+1416XXXXXXX",
"errors": []
}
}| data field | Meaning |
|---|---|
| message_id | The HonkIO message id, as returned by POST /v1/messages or listed by GET /v1/messages. |
| status | The carrier's own status from its receipt, lower case (for example delivered, sending_failed, delivery_failed or expired). |
| message_status | HonkIO's status for the message, upper case: QUEUED, SENDING, DELIVERED, FAILED, UNDELIVERED or RECEIVED. |
| to | The recipient's phone number, E.164. |
| from | Your HonkIO number the message was sent from, E.164. |
| errors | What the carrier reported, as a list; empty when it reported nothing. |
| errors[].code | The carrier's error code, as given. |
| errors[].title | A short description of the error. |
| errors[].detail | More detail, when the carrier gave any. |
message.sent
The carrier accepted the message for delivery. Fired when the send call returns, not from a receipt.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "message.sent",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"message_id": "clxxxmessagexxxxxxxxxxxxx",
"from": "+1416XXXXXXX",
"to": "+1613XXXXXXX",
"status": "SENDING",
"message_status": "SENDING",
"carrier_message_id": "40319xxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"segment_count": 1,
"cost_cents": 3
}
}| data field | Meaning |
|---|---|
| message_id | The HonkIO message id, as returned by POST /v1/messages or listed by GET /v1/messages. |
| from | Your HonkIO number the message was sent from, E.164. |
| to | The recipient's phone number, E.164. |
| status | The message status at this point, upper case (the same value as message_status). |
| message_status | HonkIO's status for the message, upper case: QUEUED, SENDING, DELIVERED, FAILED, UNDELIVERED or RECEIVED. |
| carrier_message_id | The id the carrier assigned to the message, for support requests. |
| segment_count | How many SMS segments the message was split into, which is what it was billed per. |
| cost_cents | What the message cost you, in Canadian cents. |
message.delivered
The carrier's receipt says the message reached the handset.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "message.delivered",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"message_id": "clxxxmessagexxxxxxxxxxxxx",
"status": "delivered",
"message_status": "DELIVERED",
"to": "+1613XXXXXXX",
"from": "+1416XXXXXXX",
"errors": []
}
}| data field | Meaning |
|---|---|
| message_id | The HonkIO message id, as returned by POST /v1/messages or listed by GET /v1/messages. |
| status | The carrier's own status from its receipt, lower case (for example delivered, sending_failed, delivery_failed or expired). |
| message_status | HonkIO's status for the message, upper case: QUEUED, SENDING, DELIVERED, FAILED, UNDELIVERED or RECEIVED. |
| to | The recipient's phone number, E.164. |
| from | Your HonkIO number the message was sent from, E.164. |
| errors | What the carrier reported, as a list; empty when it reported nothing. |
| errors[].code | The carrier's error code, as given. |
| errors[].title | A short description of the error. |
| errors[].detail | More detail, when the carrier gave any. |
message.failed
The message did not go out. Three shapes depending on where it failed: a failure at hand off to the carrier (CARRIER_UNAVAILABLE, CARRIER_TIMEOUT, CARRIER_ERROR, INVALID_PHONE_NUMBER, NOT_A_MOBILE_NUMBER) carries error_code and error_message but no errors array; a carrier receipt carries the carrier's raw status, an errors array and an error_code, but no error_message; a send interrupted by a server restart (error_code STALE_QUEUED) carries neither to nor from. A failure for INSUFFICIENT_BALANCE fires no webhook.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "message.failed",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"message_id": "clxxxmessagexxxxxxxxxxxxx",
"from": "+1416XXXXXXX",
"to": "+1613XXXXXXX",
"status": "FAILED",
"message_status": "FAILED",
"error_code": "INVALID_PHONE_NUMBER",
"error_message": "The destination is not a valid phone number."
}
}| data field | Meaning |
|---|---|
| message_id | The HonkIO message id, as returned by POST /v1/messages or listed by GET /v1/messages. |
| status | The carrier's own status from its receipt, lower case (for example delivered, sending_failed, delivery_failed or expired). |
| message_status | HonkIO's status for the message, upper case: QUEUED, SENDING, DELIVERED, FAILED, UNDELIVERED or RECEIVED. |
| to | The recipient's phone number, E.164. |
| from | Your HonkIO number the message was sent from, E.164. |
| error_code | HonkIO's error code for the failure, the same one the message shows in the API. |
| error_message | A readable explanation of error_code. |
| errors | What the carrier reported, as a list; empty when it reported nothing. |
| errors[].code | The carrier's error code, as given. |
| errors[].title | A short description of the error. |
| errors[].detail | More detail, when the carrier gave any. |
message.undelivered
The carrier took the message but could not deliver it: blocked, expired, or the handset was unreachable.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "message.undelivered",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"message_id": "clxxxmessagexxxxxxxxxxxxx",
"status": "delivery_failed",
"message_status": "UNDELIVERED",
"to": "+1613XXXXXXX",
"from": "+1416XXXXXXX",
"errors": [
{
"code": "40002",
"title": "Blocked as spam",
"detail": "The destination carrier blocked the message."
}
]
}
}| data field | Meaning |
|---|---|
| message_id | The HonkIO message id, as returned by POST /v1/messages or listed by GET /v1/messages. |
| status | The carrier's own status from its receipt, lower case (for example delivered, sending_failed, delivery_failed or expired). |
| message_status | HonkIO's status for the message, upper case: QUEUED, SENDING, DELIVERED, FAILED, UNDELIVERED or RECEIVED. |
| to | The recipient's phone number, E.164. |
| from | Your HonkIO number the message was sent from, E.164. |
| errors | What the carrier reported, as a list; empty when it reported nothing. |
| errors[].code | The carrier's error code, as given. |
| errors[].title | A short description of the error. |
| errors[].detail | More detail, when the carrier gave any. |
message.received
Someone texted one of your HonkIO numbers. STOP, START and HELP replies arrive here too, with keyword_action saying what was done about them.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "message.received",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"message_id": "clxxxmessagexxxxxxxxxxxxx",
"from": "+1613XXXXXXX",
"to": "+1416XXXXXXX",
"body": "Yes, see you at 3",
"keyword_action": "ignored",
"segment_count": 1,
"cost_cents": 1,
"message_status": "RECEIVED"
}
}| data field | Meaning |
|---|---|
| message_id | The HonkIO message id, as returned by POST /v1/messages or listed by GET /v1/messages. |
| from | The phone number that texted you, E.164. |
| to | Your HonkIO number that received the text, E.164. |
| body | The text of the message. |
| keyword_action | What HonkIO did about a compliance keyword, judged by the first word of the text: opted_out (STOP and the like, and an opt_out.recorded event follows), reinstated (START or UNSTOP after an opt out, and an opt_out.reinstated event follows), help (HELP, INFO or AIDE; the automatic reply was sent), or ignored. |
| segment_count | How many SMS segments the message was split into, which is what it was billed per. |
| cost_cents | What receiving the message cost you, in Canadian cents. |
| message_status | HonkIO's status for the message, upper case: QUEUED, SENDING, DELIVERED, FAILED, UNDELIVERED or RECEIVED. |
opt_out.recorded
A subscriber replied to one of your numbers with an opt out keyword (a reply whose first word is STOP, STOPALL, UNSUBSCRIBE, CANCEL, END or QUIT), and messages to them from that number are now blocked. Fires only for a keyword reply: an opt out you record with POST /v1/compliance/opt-outs does not fire it.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "opt_out.recorded",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"phone_number": "+1613XXXXXXX",
"from_number": "+1416XXXXXXX",
"keyword": "STOP"
}
}| data field | Meaning |
|---|---|
| phone_number | The subscriber who texted the keyword, E.164: the person to stop or resume messaging. |
| from_number | Your HonkIO number that received the keyword, E.164. An opt out applies to messages from this number. |
| keyword | The subscriber's reply, trimmed and upper case, e.g. STOP or STOP PLEASE. The whole reply, not only the keyword: it was recognised by its first word. |
opt_out.reinstated
A subscriber who had opted out replied with START or UNSTOP, so you may message them again from that number. Fires only for a keyword reply, and only when an opt out existed: a START from someone who never opted out is an ordinary message.received.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "opt_out.reinstated",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"phone_number": "+1613XXXXXXX",
"from_number": "+1416XXXXXXX",
"keyword": "START"
}
}| data field | Meaning |
|---|---|
| phone_number | The subscriber who texted the keyword, E.164: the person to stop or resume messaging. |
| from_number | Your HonkIO number that received the keyword, E.164. An opt out applies to messages from this number. |
| keyword | The subscriber's reply, trimmed and upper case, e.g. START. The whole reply, not only the keyword: it was recognised by its first word. |
phone_number.suspended
Monthly rent could not be charged, so the number was suspended. Top up to bring it back before the grace period ends.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "phone_number.suspended",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"phone_number": "+1416XXXXXXX",
"reason": "insufficient_balance",
"monthly_cost_cents": 299
}
}| data field | Meaning |
|---|---|
| phone_number | Your HonkIO number, E.164. |
| reason | Always insufficient_balance: the month's rent could not be charged. |
| monthly_cost_cents | The number's monthly rent, in Canadian cents. |
phone_number.released
The number has left your account and cannot be recovered. Stop routing to it.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "phone_number.released",
"created": "2026-09-24T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"phone_number": "+1416XXXXXXX",
"reason": "customer_released"
}
}| data field | Meaning |
|---|---|
| phone_number | Your HonkIO number, E.164. |
| reason | suspended_grace_expired when a suspension ran past its grace period, customer_released when you released it yourself, admin_released when HonkIO staff released it. |
Error codes
The codes below are specific to SMS, phone numbers, verification and CASL. Any call can also answer with the general codes in the platform error table, such as VALIDATION_ERROR, INSUFFICIENT_BALANCE and NOT_FOUND.
| HTTP | Code | Meaning |
|---|---|---|
| 403 | NUMBER_LIMIT_REACHED | Account has reached its phone number limit |
| 403 | PHONE_NUMBER_NOT_OWNED | The from number is not provisioned on this account. |
| 403 | ALLOW_LIST_BLOCKED | Recipient is not on the API key's ALLOW list |
| 403 | DENY_LIST_BLOCKED | Recipient is on the API key's DENY list |
| 403 | SEND_LIMIT_REQUEST_TOO_EARLY | Volume requests open once the account's probation period is over. |
| 404 | VERIFICATION_NOT_FOUND | Verification ID not found or not owned by this account |
| 404 | CONTACT_NOT_FOUND | Contact not found |
| 404 | CONTACT_GROUP_NOT_FOUND | Contact group not found |
| 404 | CONTACT_GROUP_MEMBER_NOT_FOUND | That contact is not a member of this group |
| 404 | CONTACT_LIST_NOT_FOUND | Contact list not found |
| 404 | CONTACT_LIST_ENTRY_NOT_FOUND | Contact list entry not found |
| 409 | VERIFICATION_ALREADY_VERIFIED | This number has already been verified |
| 409 | PURCHASE_IN_PROGRESS | Another number purchase is in flight. Retry shortly |
| 409 | ALLOWANCE_REQUEST_PENDING | An allowance request is already awaiting review |
| 409 | PHONE_NUMBER_SUSPENDED | This number's monthly charge could not be collected. Top up to reactivate it. |
| 409 | SEND_LIMIT_REQUEST_PENDING | A volume request is already awaiting review. |
| 409 | IDEMPOTENCY_KEY_REUSED | The same Idempotency-Key was sent with a different to, from or body. |
| 409 | CONTACT_ALREADY_EXISTS | A contact with that phone number already exists |
| 409 | CONTACT_GROUP_ALREADY_EXISTS | A contact group with that name already exists |
| 409 | CONTACT_GROUP_MEMBER_EXISTS | Contact is already a member of this group |
| 409 | CONTACT_LIST_ALREADY_EXISTS | A contact list with that name already exists |
| 409 | CONTACT_LIST_ENTRY_EXISTS | This entry already exists in the list |
| 410 | VERIFICATION_EXPIRED | The verification code has expired |
| 422 | NON_CANADIAN_NUMBER | Not a valid Canadian E.164 number |
| 422 | UNDELIVERABLE_NUMBER | The number failed at the carrier three times running (for any customer); refused without charge for 90 days. |
| 422 | NOT_A_MOBILE_NUMBER | The destination is a landline or VoIP number. The carrier refuses it before sending; nothing is charged. |
| 422 | RESERVED_DESTINATION | The destination is in a reserved exchange (555-XXXX, N11, carrier test codes). Refused before anything is sent; nothing is charged. |
| 422 | MESSAGE_TOO_LONG | Body would exceed the carrier limit of 10 SMS parts (≈1,530 GSM-7 or 670 Unicode characters). Nothing is charged |
| 422 | VERIFICATION_INVALID_CODE | Incorrect code. attempts_remaining shows how many tries are left |
| 422 | INVALID_ALLOWANCE_REQUEST | Requested allowance must exceed your current limit |
| 422 | LINK_SHORTENER_BLOCKED | Link shorteners are refused because carriers filter them. Use the full URL |
| 422 | BROADCAST_TOO_LARGE | Contact group too large for one broadcast (250 members by default). Split it and send in batches |
| 422 | CANNOT_ERASE_OWN_NUMBER | That number belongs to your account. Erasure is only for a subscriber's number |
| 422 | TOO_MANY_AREA_CODES | Search at most 25 area codes at a time |
| 422 | INVALID_PHONE_NUMBER | Not a valid E.164 phone number |
| 422 | CONTACT_LIST_ENTRY_INVALID | Each entry must specify exactly one of phone_number, contact_id or contact_group_id |
| 429 | RECIPIENT_RATE_LIMITED | More than 30 messages to one recipient in an hour or 100 in a day; retry after the Retry-After header. |
| 429 | VERIFICATION_MAX_ATTEMPTS | Too many wrong attempts. This verification is locked |
| 429 | DAILY_LIMIT_REACHED | Daily sending limit reached (details show your limit and count). Request a higher volume once eligible |
| 429 | FANOUT_LIMIT_REACHED | This exact message already reached the maximum number of distinct recipients allowed in 24 hours |
| 429 | NUMBER_RATE_LIMITED | This sending number reached its per-minute limit. Retry shortly |
| 429 | BROADCAST_LIMIT_REACHED | Broadcast limit reached for this account in the last 24 hours |
| 451 | OPT_OUT_BLOCKED | Recipient has opted out, so the send is legally blocked |
| 451 | NO_CONSENT | No valid CASL consent on file |
| 451 | CONSENT_EXPIRED | Implied consent expired (2-year CASL limit) |
| 451 | DNCL_BLOCKED | Number on the CRTC DNCL with no exemption (coming soon; not currently returned) |
| 501 | DNCL_COMING_SOON | CRTC Do Not Call List checking is not available yet. |
| 501 | TOLLFREE_800_COMING_SOON | 1-800 numbers are not available yet. Choose another toll-free prefix or a local number. |
| 502 | CARRIER_ERROR | The carrier returned an error while sending. |
| 502 | PROVISIONING_FAILED | The carrier could not complete this number purchase. Nothing was charged. |
| 502 | NUMBER_SEARCH_FAILED | Number search is temporarily unavailable. |
| 502 | RELEASE_FAILED | The carrier did not accept the release. The number is still yours; retry shortly |
| 503 | CARRIER_UNAVAILABLE | The carrier could not be reached; nothing was sent or charged. Retry shortly. |
| 503 | CARRIER_TIMEOUT | The carrier did not answer in time, so the message may or may not have been sent. Nothing is charged. Check the message status before sending again: a retry with the same Idempotency-Key returns the failed message instead of sending it again. |
HonkIO