API reference
Messages
Send and receive SMS messages.
/v1/messagesList messages
messages:rNode.js SDKhonkio.messages.list()Requires messages:r.
Parameters
| Parameter | Type | Description |
|---|---|---|
qquery | string | Search by phone number (partial match on to/from) |
fromquery | string | |
toquery | string | |
statusquery | string |
|
directionquery | string |
|
modequery | string | Only messages sent in this mode. Without it, a live key lists both modes (each message carries its own
|
pagequery | integer |
|
limitquery | integer |
|
date_fromquery | string |
|
date_toquery | string |
|
Responses
200A page of messages, newest first.
Field Type Description data[]requiredobject[] idrequiredstring directionrequiredstring - One of: OUTBOUND | INBOUND
fromrequiredstring torequiredstring bodyrequiredstring Null once the body is purged (see body_purged) and on a verification OTP, whose code is never stored.
- Can be null
body_purgedrequiredboolean statusrequiredstring SCHEDULED: accepted with scheduled_at and waiting for it. CANCELLED: a scheduled message cancelled before it went out; its charge was refunded.
- One of: QUEUED | SCHEDULED | SENDING | SENT | DELIVERED | FAILED | UNDELIVERED | RECEIVED | CANCELLED
segment_countrequiredinteger Number of SMS segments the carrier split the body into (inbound: as received). Billing is per segment.
- Can be null
moderequiredstring TEST messages are simulated: never sent to a carrier and never billed.
- One of: LIVE | TEST
is_verificationrequiredboolean The OTP SMS behind a verification. Its body is never stored.
auto_reply_keywordrequiredstring Set on the automatic confirmation sent when a recipient texts STOP, START or HELP. You did not send it and are never charged for it. Its body can be null for a few minutes after it is sent, until the carrier reports the text. Null on every other message.
- One of: STOP | START | HELP
- Can be null
cost_centsrequiredinteger What this message actually cost, in cents CAD: message_cost_cents times the segments the carrier split it into, settled to the carrier's count once it accepts the send (so it can differ from a pre-send estimate). 0 when the carrier refused the send outright (refunded or never charged), and on an automatic STOP/START/HELP confirmation (auto_reply_keyword), which is never charged. A message the carrier accepted but could not deliver keeps its charge. Inbound messages carry the inbound charge. On a TEST message this is what the send would have cost; nothing was charged.
error_coderequiredstring Set on FAILED and UNDELIVERED messages: the carrier's code from the delivery receipt (e.g. 30003), or the platform code for a synchronous rejection (CARRIER_ERROR, INVALID_PHONE_NUMBER, NOT_A_MOBILE_NUMBER, INSUFFICIENT_BALANCE, RENT_OWED). CARRIER_THROTTLED when the carrier asked us to slow down, or dropped the message from the sending number's full queue before trying the recipient: refunded, and safe to send again. CARRIER_LOST when the carrier accepted the message and then lost it: no receipt ever came, and three days later the carrier had no record of it; refunded, and safe to send again with a new Idempotency-Key.
- Can be null
error_messagerequiredstring Human-readable reason for a FAILED or UNDELIVERED message, from the carrier when it gave one.
- Can be null
sent_atrequiredstring - Format: date-time
- Can be null
created_atrequiredstring - Format: date-time
casl_consent_typerequiredstring The CASL consent an outbound send went out on. Null for inbound and test-mode messages, carrier-mandated STOP/HELP/START replies, and sends made before this was recorded (18 September 2026).
- One of: EXPRESS | IMPLIED
- Can be null
delivered_atrequiredstring When the carrier reported the message delivered to the handset.
- Format: date-time
- Can be null
scheduled_atrequiredstring When a scheduled message is (or was) due to go out. Null on a message sent at once.
- Format: date-time
- Can be null
expires_atrequiredstring The expires_at a scheduled message was given: the latest it may go out. Null when none was given, which means scheduled_at plus 24 hours. One that cannot go out by then fails with error_code SCHEDULED_SEND_EXPIRED and is refunded.
- Format: date-time
- Can be null
metarequiredobject Page-number pagination, returned as
metaby the messages listing.pagerequiredinteger limitrequiredinteger totalrequiredinteger pagesrequiredinteger - 401Missing, invalid, revoked or expired API key.The standard error body.
- 402The account has no balance, or a LIVE key was used before the first top-up.The standard error body.
- 403The API key lacks the permission this operation requires, or the account is suspended or closed.The standard error body.
- 422VALIDATION_ERROR: the request failed validation; details names the fields.The standard error body.
- 429Rate limit reached: 100 requests per second per account, or a route-specific limit (Retry-After set where one applies).The standard error body.
- 500Unexpected server error.The standard error body.
Example
const { data, error } = await honkio.messages.list({ direction: 'INBOUND', limit: 20 })
if (error) throw new Error(error.message)
for (const message of data.data) console.log(message.from, message.body)curl https://api.honkio.ca/v1/messages \
-H "Authorization: Bearer mk_live_YOUR_KEY"/v1/messagesSend an SMS message
messages:wNode.js SDKhonkio.messages.send()Live sends are gated before any charge: 403 SENDING_PAUSED, 429 DAILY_LIMIT_REACHED / NUMBER_RATE_LIMITED / RECIPIENT_RATE_LIMITED (NUMBER_RATE_LIMITED: the sending number already has a minute of message segments waiting for the carrier, which sends 10 segments a minute from a local number; RECIPIENT_RATE_LIMITED: 30 messages an hour and 100 a day to one recipient; Retry-After is set on both) / FANOUT_LIMIT_REACHED, 422 UNDELIVERABLE_NUMBER (three consecutive carrier failures to the number, from any customer, list it for 90 days), 422 NOT_A_MOBILE_NUMBER (a landline or VoIP destination, refused by the carrier before sending), 422 RESERVED_DESTINATION (a reserved exchange such as 555-XXXX, N11 or a carrier test code, refused here in both modes), 422 LINK_SHORTENER_BLOCKED, and the CASL codes. From a toll-free number: 403 TOLL_FREE_NOT_VERIFIED until the number's toll-free verification is approved (test keys too), and 451 DOUBLE_OPT_IN_REQUIRED until the recipient has replied YES to an opt-in confirmation from that number (POST /v1/compliance/opt-in-confirmations). A refused send costs nothing. 503 CARRIER_UNAVAILABLE means the carrier could not be reached; nothing was sent or charged, retry shortly. 503 CARRIER_THROTTLED means the carrier asked us to slow down and did not take the message: nothing was charged; retry after Retry-After with the same Idempotency-Key, which sends it. 503 CARRIER_TIMEOUT means the carrier did not answer in time: the message may or may not have been sent, its charge is held and refunded once the carrier's records show it was not sent, and a retry with the same Idempotency-Key returns the failed message rather than sending it again. Send an Idempotency-Key header from anything that retries: the same key returns the original message. A live send answers estimated_send_at, an estimate of when the carrier will send it. With scheduled_at (1 minute to 30 days ahead) the message is accepted with status SCHEDULED and charged now; every check above except the sending limits runs now, and all of them run again when it is due: a recipient who opted out since, a revoked key, a suspended number or account, or rent owed fails it (refunded, message.failed), while a sending limit, a pause, or the carrier throttling it or being unreachable holds it and tries again until it can go. It must go out by expires_at (1 minute to 24 hours after scheduled_at; by default scheduled_at plus 24 hours): one that cannot fails with error_code SCHEDULED_SEND_EXPIRED, refunded, with message.failed. Until then PATCH /v1/messages/{id} moves it and DELETE /v1/messages/{id} cancels it and refunds the charge. In place of body, template (a template's id or alias), variables and language (en or fr) send that template's SMS part, rendered: a live key the published version, a test key the draft. A language without an SMS part uses the default language's; a template with no SMS part answers 422 TEMPLATE_CHANNEL_MISSING. The rendered text is held to the 1600 characters body is, and is what is stored, charged and sent.
Requires messages:w.
Parameters
| Parameter | Type | Description |
|---|---|---|
Idempotency-Keyheader | string |
|
Request body
| Field | Type | Description |
|---|---|---|
fromrequired | string | Sending number (E.164, must be owned by account) |
torequired | string | Recipient number (E.164, Canadian numbers only) |
body | string | Up to 1600 characters and 10 SMS segments (about 1,530 GSM-7 or 670 Unicode characters). Longer bodies are rejected with 422 MESSAGE_TOO_LONG before any charge. Billed per segment as the carrier splits it. Required unless you send a template; not with one.
|
template | string | A template's id or alias (/v1/templates). Its part for this channel is rendered and sent in place of your own text. A live key sends the published version, a test key the draft. With an Idempotency-Key, a retry is matched on the request exactly as sent: the template's id and its alias, 1 and "1" as a variable value, and a language left out and the default language named all count as different requests (409 IDEMPOTENCY_KEY_REUSED).
|
variables | object | Values for the template's variables. A variable with neither a value nor a fallback refuses the send (422 EMAIL_TEMPLATE_VARIABLE_MISSING, details.keys). Only with template. |
language | string | en or fr: the language version to render. A part that language lacks comes from the template's default language. Leave it out for the default language. Only with template.
|
skip_consent_check | boolean | Skip the CASL consent gate. Test-mode keys only: a live key gets 403 FORBIDDEN. Use only when you have consent recorded outside HonkIO. |
scheduled_at | string | Send later: an ISO 8601 date-time with an offset, 1 minute to 30 days ahead.
|
expires_at | string | The latest the scheduled send may go out: 1 minute to 24 hours after scheduled_at, and only with it. Without it, scheduled_at plus 24 hours. A send that cannot go out by then fails with SCHEDULED_SEND_EXPIRED and is refunded.
|
Responses
201Message sent (LIVE) or simulated (TEST). With an Idempotency-Key already used for the same from, to and body, the original message.
Field Type Description idrequiredstring directionrequiredstring - One of: OUTBOUND | INBOUND
fromrequiredstring torequiredstring bodyrequiredstring Null once the body is purged (see body_purged) and on a verification OTP, whose code is never stored.
- Can be null
body_purgedrequiredboolean statusrequiredstring SCHEDULED: accepted with scheduled_at and waiting for it. CANCELLED: a scheduled message cancelled before it went out; its charge was refunded.
- One of: QUEUED | SCHEDULED | SENDING | SENT | DELIVERED | FAILED | UNDELIVERED | RECEIVED | CANCELLED
segment_countrequiredinteger Number of SMS segments the carrier split the body into (inbound: as received). Billing is per segment.
- Can be null
moderequiredstring TEST messages are simulated: never sent to a carrier and never billed.
- One of: LIVE | TEST
is_verificationrequiredboolean The OTP SMS behind a verification. Its body is never stored.
auto_reply_keywordrequiredstring Set on the automatic confirmation sent when a recipient texts STOP, START or HELP. You did not send it and are never charged for it. Its body can be null for a few minutes after it is sent, until the carrier reports the text. Null on every other message.
- One of: STOP | START | HELP
- Can be null
cost_centsrequiredinteger What this message actually cost, in cents CAD: message_cost_cents times the segments the carrier split it into, settled to the carrier's count once it accepts the send (so it can differ from a pre-send estimate). 0 when the carrier refused the send outright (refunded or never charged), and on an automatic STOP/START/HELP confirmation (auto_reply_keyword), which is never charged. A message the carrier accepted but could not deliver keeps its charge. Inbound messages carry the inbound charge. On a TEST message this is what the send would have cost; nothing was charged.
error_coderequiredstring Set on FAILED and UNDELIVERED messages: the carrier's code from the delivery receipt (e.g. 30003), or the platform code for a synchronous rejection (CARRIER_ERROR, INVALID_PHONE_NUMBER, NOT_A_MOBILE_NUMBER, INSUFFICIENT_BALANCE, RENT_OWED). CARRIER_THROTTLED when the carrier asked us to slow down, or dropped the message from the sending number's full queue before trying the recipient: refunded, and safe to send again. CARRIER_LOST when the carrier accepted the message and then lost it: no receipt ever came, and three days later the carrier had no record of it; refunded, and safe to send again with a new Idempotency-Key.
- Can be null
error_messagerequiredstring Human-readable reason for a FAILED or UNDELIVERED message, from the carrier when it gave one.
- Can be null
sent_atrequiredstring - Format: date-time
- Can be null
created_atrequiredstring - Format: date-time
casl_consent_typerequiredstring The CASL consent an outbound send went out on. Null for inbound and test-mode messages, carrier-mandated STOP/HELP/START replies, and sends made before this was recorded (18 September 2026).
- One of: EXPRESS | IMPLIED
- Can be null
delivered_atrequiredstring When the carrier reported the message delivered to the handset.
- Format: date-time
- Can be null
scheduled_atrequiredstring When a scheduled message is (or was) due to go out. Null on a message sent at once.
- Format: date-time
- Can be null
expires_atrequiredstring The expires_at a scheduled message was given: the latest it may go out. Null when none was given, which means scheduled_at plus 24 hours. One that cannot go out by then fails with error_code SCHEDULED_SEND_EXPIRED and is refunded.
- Format: date-time
- Can be null
estimated_send_atrequiredstring An estimate of when the carrier will start sending this message, from the segments already waiting on the sending number (a local number sends 10 segments a minute). Close to now when nothing is waiting. For a live scheduled send, its scheduled_at. Null in test mode and on a retry that returned an earlier send.
- Format: date-time
- Can be null
- 401Missing, invalid, revoked or expired API key.The standard error body.
- 402INSUFFICIENT_BALANCE: the balance cannot cover the send. RENT_OWED: rent is owed on a number released while suspended; top up to pay it first. Also PAYMENT_REQUIRED when a LIVE key is used before the first top-up.The standard error body.
- 403FORBIDDEN (skip_consent_check on a live key, or the key lacks messages:w), ACCOUNT_NOT_VERIFIED, PHONE_NUMBER_NOT_OWNED, ALLOW_LIST_BLOCKED, DENY_LIST_BLOCKED, SENDING_PAUSED, or TOLL_FREE_NOT_VERIFIED (a toll-free from number without an approved toll-free verification; test keys too).The standard error body.
- 409PHONE_NUMBER_SUSPENDED (the from number is suspended for unpaid rent) or IDEMPOTENCY_KEY_REUSED (the key was used for a different from, to or body).The standard error body.
- 422VALIDATION_ERROR, NON_CANADIAN_NUMBER, RESERVED_DESTINATION, MESSAGE_TOO_LONG, LINK_SHORTENER_BLOCKED, UNDELIVERABLE_NUMBER, NOT_A_MOBILE_NUMBER, INVALID_PHONE_NUMBER (the carrier found the number invalid) or IDEMPOTENCY_KEY_RESERVED (the Idempotency-Key starts with notify:, a prefix HonkIO reserves). A refused send costs nothing.The standard error body.
- 429DAILY_LIMIT_REACHED, FANOUT_LIMIT_REACHED, NUMBER_RATE_LIMITED or RECIPIENT_RATE_LIMITED (Retry-After set on the two rate limits), or RATE_LIMITED at 100 requests per second. NUMBER_RATE_LIMITED: the sending number already has a minute of message segments waiting for the carrier (a local number sends 10 segments a minute); wait Retry-After. SCHEDULED_LIMIT_REACHED (with scheduled_at): the account already holds 10,000 scheduled live messages, or 1,000 scheduled test messages; nothing is scheduled or charged (details carry mode, limit, scheduled and requested).The standard error body.
- 451The CASL gate refused the recipient: NO_CONSENT, OPT_OUT_BLOCKED or CONSENT_EXPIRED. From a toll-free number, also DOUBLE_OPT_IN_REQUIRED: the recipient has not yet replied YES to an opt-in confirmation from that number (POST /v1/compliance/opt-in-confirmations).The standard error body.
- 500Unexpected server error.The standard error body.
- 502CARRIER_ERROR: the carrier refused the send. Any charge is refunded.The standard error body.
- 503CARRIER_UNAVAILABLE: the carrier could not be reached. Nothing was sent or charged; retry shortly. CARRIER_THROTTLED: the carrier asked us to slow down and did not take the message. Nothing was charged; retry after Retry-After with the same Idempotency-Key, which sends it. CARRIER_TIMEOUT: the carrier did not answer in time, so the message may or may not have been sent. Its charge is held, and refunded once the carrier's records show it was not sent. Check the message status before sending again: a retry with the same Idempotency-Key returns the failed message instead of sending it again.The standard error body.
Example
const { data, error } = await honkio.messages.send({
from: '+1416XXXXXXX', // one of your HonkIO numbers
to: '+1613XXXXXXX',
body: 'Your order has shipped.',
})
if (error) throw new Error(`${error.name}: ${error.message}`)
console.log(data.id, data.status)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"
}'/v1/messages/{id}Retrieve a message by ID
messages:rNode.js SDKhonkio.messages.get()Requires messages:r.
Parameters
| Parameter | Type | Description |
|---|---|---|
idrequiredpath | string |
Responses
200The message.
Field Type Description idrequiredstring directionrequiredstring - One of: OUTBOUND | INBOUND
fromrequiredstring torequiredstring bodyrequiredstring Null once the body is purged (see body_purged) and on a verification OTP, whose code is never stored.
- Can be null
body_purgedrequiredboolean statusrequiredstring SCHEDULED: accepted with scheduled_at and waiting for it. CANCELLED: a scheduled message cancelled before it went out; its charge was refunded.
- One of: QUEUED | SCHEDULED | SENDING | SENT | DELIVERED | FAILED | UNDELIVERED | RECEIVED | CANCELLED
segment_countrequiredinteger Number of SMS segments the carrier split the body into (inbound: as received). Billing is per segment.
- Can be null
moderequiredstring TEST messages are simulated: never sent to a carrier and never billed.
- One of: LIVE | TEST
is_verificationrequiredboolean The OTP SMS behind a verification. Its body is never stored.
auto_reply_keywordrequiredstring Set on the automatic confirmation sent when a recipient texts STOP, START or HELP. You did not send it and are never charged for it. Its body can be null for a few minutes after it is sent, until the carrier reports the text. Null on every other message.
- One of: STOP | START | HELP
- Can be null
cost_centsrequiredinteger What this message actually cost, in cents CAD: message_cost_cents times the segments the carrier split it into, settled to the carrier's count once it accepts the send (so it can differ from a pre-send estimate). 0 when the carrier refused the send outright (refunded or never charged), and on an automatic STOP/START/HELP confirmation (auto_reply_keyword), which is never charged. A message the carrier accepted but could not deliver keeps its charge. Inbound messages carry the inbound charge. On a TEST message this is what the send would have cost; nothing was charged.
error_coderequiredstring Set on FAILED and UNDELIVERED messages: the carrier's code from the delivery receipt (e.g. 30003), or the platform code for a synchronous rejection (CARRIER_ERROR, INVALID_PHONE_NUMBER, NOT_A_MOBILE_NUMBER, INSUFFICIENT_BALANCE, RENT_OWED). CARRIER_THROTTLED when the carrier asked us to slow down, or dropped the message from the sending number's full queue before trying the recipient: refunded, and safe to send again. CARRIER_LOST when the carrier accepted the message and then lost it: no receipt ever came, and three days later the carrier had no record of it; refunded, and safe to send again with a new Idempotency-Key.
- Can be null
error_messagerequiredstring Human-readable reason for a FAILED or UNDELIVERED message, from the carrier when it gave one.
- Can be null
sent_atrequiredstring - Format: date-time
- Can be null
created_atrequiredstring - Format: date-time
casl_consent_typerequiredstring The CASL consent an outbound send went out on. Null for inbound and test-mode messages, carrier-mandated STOP/HELP/START replies, and sends made before this was recorded (18 September 2026).
- One of: EXPRESS | IMPLIED
- Can be null
delivered_atrequiredstring When the carrier reported the message delivered to the handset.
- Format: date-time
- Can be null
scheduled_atrequiredstring When a scheduled message is (or was) due to go out. Null on a message sent at once.
- Format: date-time
- Can be null
expires_atrequiredstring The expires_at a scheduled message was given: the latest it may go out. Null when none was given, which means scheduled_at plus 24 hours. One that cannot go out by then fails with error_code SCHEDULED_SEND_EXPIRED and is refunded.
- Format: date-time
- Can be null
- 401Missing, invalid, revoked or expired API key.The standard error body.
- 402The account has no balance, or a LIVE key was used before the first top-up.The standard error body.
- 403The API key lacks the permission this operation requires, or the account is suspended or closed.The standard error body.
- 404NOT_FOUND: no such message on this account. A test key finds TEST messages only.The standard error body.
- 429Rate limit reached: 100 requests per second per account, or a route-specific limit (Retry-After set where one applies).The standard error body.
- 500Unexpected server error.The standard error body.
Example
const { data, error } = await honkio.messages.get('MESSAGE_ID')
if (error) throw new Error(error.message)
console.log(data.status, data.cost_cents)curl https://api.honkio.ca/v1/messages/ID \
-H "Authorization: Bearer mk_live_YOUR_KEY"/v1/messages/{id}Reschedule a scheduled message
messages:mNode.js SDKhonkio.messages.update()Only while the message is SCHEDULED (otherwise 422 MESSAGE_NOT_SCHEDULED). The charge is unchanged. Sends message.rescheduled.
Requires messages:m.
Parameters
| Parameter | Type | Description |
|---|---|---|
idrequiredpath | string |
Request body
The new send time of a scheduled message.
| Field | Type | Description |
|---|---|---|
scheduled_atrequired | string | An ISO 8601 date-time with an offset, 1 minute to 30 days ahead.
|
expires_at | string | The latest it may go out: 1 minute to 24 hours after the new scheduled_at, or null for scheduled_at plus 24 hours. Left out, the stored expires_at is kept if it still fits the new scheduled_at, and dropped (back to the 24 hour default) if it does not.
|
Responses
200The message's new send time.
Field Type Description idrequiredstring statusrequiredstring - One of: SCHEDULED
scheduled_atrequiredstring - Format: date-time
expires_atrequiredstring The expires_at now stored, or null for scheduled_at plus 24 hours.
- Format: date-time
- Can be null
- 401Missing, invalid, revoked or expired API key.The standard error body.
- 402The account has no balance, or a LIVE key was used before the first top-up.The standard error body.
- 403The API key lacks the permission this operation requires, or the account is suspended or closed.The standard error body.
- 404NOT_FOUND (details.resource "message"): no outbound message with that id in the calling key's mode.The standard error body.
- 422MESSAGE_NOT_SCHEDULED (only a SCHEDULED message can be moved; details.current_status says what it is now) or VALIDATION_ERROR (scheduled_at must be 1 minute to 30 days ahead; expires_at 1 minute to 24 hours after it).The standard error body.
- 429Rate limit reached: 100 requests per second per account, or a route-specific limit (Retry-After set where one applies).The standard error body.
- 500Unexpected server error.The standard error body.
Example
const { data, error } = await honkio.messages.update('MESSAGE_ID', { scheduledAt: '2026-10-01T14:00:00Z' })
if (error) throw new Error(error.message)
console.log(data.scheduled_at, data.expires_at) // expires_at: null means scheduled_at plus 24 hourscurl -X PATCH https://api.honkio.ca/v1/messages/ID \
-H "Authorization: Bearer mk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduled_at": "2026-10-01T14:00:00Z"
}'/v1/messages/{id}Cancel a scheduled message
messages:dNode.js SDKhonkio.messages.cancel()Only while the message is SCHEDULED (otherwise 422 MESSAGE_NOT_SCHEDULED). Its charge is refunded and message.cancelled is sent. Cannot be undone.
Requires messages:d.
Parameters
| Parameter | Type | Description |
|---|---|---|
idrequiredpath | string |
Responses
- 204Cancelled: the charge refunded.
- 401Missing, invalid, revoked or expired API key.The standard error body.
- 402The account has no balance, or a LIVE key was used before the first top-up.The standard error body.
- 403The API key lacks the permission this operation requires, or the account is suspended or closed.The standard error body.
- 404NOT_FOUND (details.resource "message"): no outbound message with that id in the calling key's mode.The standard error body.
- 422MESSAGE_NOT_SCHEDULED: only a SCHEDULED message can be cancelled; details.current_status says what it is now.The standard error body.
- 429Rate limit reached: 100 requests per second per account, or a route-specific limit (Retry-After set where one applies).The standard error body.
- 500Unexpected server error.The standard error body.
Example
const { error } = await honkio.messages.cancel('MESSAGE_ID')
if (error) throw new Error(error.message)curl -X DELETE https://api.honkio.ca/v1/messages/ID \
-H "Authorization: Bearer mk_live_YOUR_KEY"
HonkIO