honkio-ca Messages API

Send and receive SMS messages

Operations 3

POST /v1/messages Send an SMS message #
GET /v1/messages List messages #
GET /v1/messages/{id} Retrieve a message by ID #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/honkio-ca:honkio-ca-messages-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

honkio-ca-messages-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: HonkIO Messages API
  description: 'Canadian-focused SMS gateway API.


    ## Canadian Compliance

    - **CASL**: All commercial messages require consent. Use `/v1/compliance/consents` to record.

    - **DNCL**: CRTC Do Not Call List checking is coming soon (not yet enforced).

    - **PIPEDA / Quebec Law 25**: Data is stored in Canada (ca-central-1). Requests are processed in the United States in transit. Right-to-erasure available.


    ## Authentication

    Provide your API key via `Authorization: Bearer mk_live_...` or `X-API-Key: mk_live_...`.

    Test keys never send, never charge and can''t change live data; they work before your first top-up.


    ## Rate limits

    100 requests per second per account. Every authenticated response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.


    ## Pagination

    Shape follows the route family, not one convention across the API: documented here rather than changed. Messages listings return `{ data, meta: { page, limit, total, pages } }`. Contacts, Contact Groups, Lists and Verify listings return `{ data, total, limit, offset }`. Compliance consent and opt-out listings accept `page`/`limit` and return `{ data }`, with no total. Account transactions use a cursor: pass `before`, get back `has_more`.'
  version: 1.0.0
  contact:
    name: HonkIO Support
    email: support@honkio.ca
servers:
- url: https://api.honkio.ca
  description: Production
- url: http://localhost:3000
  description: Local development
security:
- apiKey: []
- apiKeyHeader: []
tags:
- name: Messages
  description: Send and receive SMS messages
paths:
  /v1/messages:
    post:
      summary: Send an SMS message
      tags:
      - Messages
      description: '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 parts waiting for the carrier, which sends 10 parts 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, nothing was charged, 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.


        Requires `messages:w`.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - from
              - to
              - body
              properties:
                from:
                  type: string
                  description: Sending number (E.164, must be owned by account)
                to:
                  type: string
                  description: Recipient number (E.164, Canadian numbers only)
                body:
                  type: string
                  maxLength: 1600
                  description: Up to 1600 characters and 10 SMS parts (about 1,530 GSM-7 or 670 Unicode characters). Longer bodies are rejected with 422 MESSAGE_TOO_LONG before any charge. Billed per part as the carrier splits it.
                skip_consent_check:
                  type: boolean
                  description: 'Skip the CASL consent gate. Test-mode keys only: a live key gets 403 FORBIDDEN. Use only when you have consent recorded outside HonkIO.'
                dncl_exemptions:
                  type: array
                  items:
                    type: string
                    enum:
                    - existing_business_relationship
                    - registered_charity
                    - political
                    - survey
                    - newspaper_subscription
                    - personal
                  description: DNCL exemption reasons. CRTC DNCL checking is coming soon and not yet enforced, so this field is currently accepted but not acted upon.
      parameters:
      - schema:
          type: string
          maxLength: 255
        in: header
        name: idempotency-key
        required: false
      security:
      - apiKey: []
      - apiKeyHeader: []
      x-honkio-permission: messages:w
      responses:
        '201':
          description: Message sent (LIVE) or simulated (TEST). With an Idempotency-Key already used for the same from, to and body, the original message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SentMessage'
        '401':
          description: Missing, invalid, revoked or expired API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: 'INSUFFICIENT_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.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: FORBIDDEN (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).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: PHONE_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).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: VALIDATION_ERROR, NON_CANADIAN_NUMBER, RESERVED_DESTINATION, MESSAGE_TOO_LONG, LINK_SHORTENER_BLOCKED, UNDELIVERABLE_NUMBER, NOT_A_MOBILE_NUMBER or INVALID_PHONE_NUMBER (the carrier found the number invalid). A refused send costs nothing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: 'DAILY_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 parts waiting for the carrier (a local number sends 10 parts a minute); wait Retry-After.'
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait before retrying.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '451':
          description: 'The 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).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          description: 'CARRIER_ERROR: the carrier refused the send. Any charge is refunded.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: 'CARRIER_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. 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.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      operationId: postV1Messages
      x-operation-id-source: derived
    get:
      summary: List messages
      tags:
      - Messages
      description: Requires `messages:r`.
      parameters:
      - schema:
          type: string
        in: query
        name: q
        required: false
        description: Search by phone number (partial match on to/from)
      - schema:
          type: string
        in: query
        name: from
        required: false
      - schema:
          type: string
        in: query
        name: to
        required: false
      - schema:
          type: string
          enum:
          - QUEUED
          - SENDING
          - SENT
          - DELIVERED
          - FAILED
          - UNDELIVERED
          - RECEIVED
        in: query
        name: status
        required: false
      - schema:
          type: string
          enum:
          - OUTBOUND
          - INBOUND
        in: query
        name: direction
        required: false
      - schema:
          type: integer
          minimum: 1
          default: 1
        in: query
        name: page
        required: false
      - schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 20
        in: query
        name: limit
        required: false
      - schema:
          type: string
          format: date
        in: query
        name: date_from
        required: false
      - schema:
          type: string
          format: date
        in: query
        name: date_to
        required: false
      security:
      - apiKey: []
      - apiKeyHeader: []
      x-honkio-permission: messages:r
      responses:
        '200':
          description: A page of messages, newest first.
          content:
            application/json:
              schema:
                description: A page of messages, newest first.
                type: object
                additionalProperties: true
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Message'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
        '401':
          description: Missing, invalid, revoked or expired API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: The account has no balance, or a LIVE key was used before the first top-up.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: The API key lacks the permission this operation requires, or the account is suspended or closed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: 'VALIDATION_ERROR: the request failed validation; details names the fields.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: 'Rate limit reached: 100 requests per second per account, or a route-specific limit (Retry-After set where one applies).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      operationId: getV1Messages
      x-operation-id-source: derived
  /v1/messages/{id}:
    get:
      summary: Retrieve a message by ID
      tags:
      - Messages
      description: Requires `messages:r`.
      parameters:
      - schema:
          type: string
        in: path
        name: id
        required: true
      security:
      - apiKey: []
      - apiKeyHeader: []
      x-honkio-permission: messages:r
      responses:
        '200':
          description: The message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
        '401':
          description: Missing, invalid, revoked or expired API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: The account has no balance, or a LIVE key was used before the first top-up.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: The API key lacks the permission this operation requires, or the account is suspended or closed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: 'NOT_FOUND: no such message on this account. A test key finds TEST messages only.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: 'Rate limit reached: 100 requests per second per account, or a route-specific limit (Retry-After set where one applies).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      operationId: getV1MessagesById
      x-operation-id-source: derived
components:
  schemas:
    SentMessage:
      type: object
      additionalProperties: true
      required:
      - id
      - direction
      - from
      - to
      - body
      - body_purged
      - status
      - segment_count
      - mode
      - is_verification
      - auto_reply_keyword
      - cost_cents
      - error_code
      - error_message
      - sent_at
      - created_at
      - casl_consent_type
      - dncl_exemption
      - delivered_at
      - estimated_send_at
      properties:
        id:
          type: string
        direction:
          type: string
          enum:
          - OUTBOUND
          - INBOUND
        from:
          type: string
        to:
          type: string
        body:
          type:
          - string
          - 'null'
          description: Null once the body is purged (see body_purged) and on a verification OTP, whose code is never stored.
        body_purged:
          type: boolean
        status:
          type: string
          enum:
          - QUEUED
          - SENDING
          - SENT
          - DELIVERED
          - FAILED
          - UNDELIVERED
          - RECEIVED
        segment_count:
          type:
          - integer
          - 'null'
          description: 'Number of SMS parts the carrier split the body into (inbound: as received). Billing is per part.'
        mode:
          type: string
          enum:
          - LIVE
          - TEST
          description: 'TEST messages are simulated: never sent to a carrier and never billed.'
        is_verification:
          type: boolean
          description: The OTP SMS behind a verification. Its body is never stored.
        auto_reply_keyword:
          type:
          - string
          - 'null'
          enum:
          - STOP
          - START
          - HELP
          - null
          description: 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.
        cost_cents:
          type: integer
          description: 'What this message actually cost, in cents CAD: message_cost_cents times the parts 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_code:
          type:
          - string
          - 'null'
          description: '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.'
        error_message:
          type:
          - string
          - 'null'
          description: Human-readable reason for a FAILED or UNDELIVERED message, from the carrier when it gave one.
        sent_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        casl_consent_type:
          type:
          - string
          - 'null'
          enum:
          - EXPRESS
          - IMPLIED
          - null
          description: 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).
        dncl_exemption:
          type:
          - string
          - 'null'
          description: The CRTC Do Not Call List exemption the send relied on, when one applied.
        delivered_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When the carrier reported the message delivered to the handset.
        estimated_send_at:
          type:
          - string
          - 'null'
          format: date-time
          description: An estimate of when the carrier will start sending this message, from the parts already waiting on the sending number (a local number sends 10 parts a minute). Close to now when nothing is waiting. Null in test mode and on a retry that returned an earlier send.
      description: The message a send created, with an estimate of when the carrier will send it.
    Message:
      type: object
      additionalProperties: true
      required:
      - id
      - direction
      - from
      - to
      - body
      - body_purged
      - status
      - segment_count
      - mode
      - is_verification
      - auto_reply_keyword
      - cost_cents
      - error_code
      - error_message
      - sent_at
      - created_at
      - casl_consent_type
      - dncl_exemption
      - delivered_at
      properties:
        id:
          type: string
        direction:
          type: string
          enum:
          - OUTBOUND
          - INBOUND
        from:
          type: string
        to:
          type: string
        body:
          type:
          - string
          - 'null'
          description: Null once the body is purged (see body_purged) and on a verification OTP, whose code is never stored.
        body_purged:
          type: boolean
        status:
          type: string
          enum:
          - QUEUED
          - SENDING
          - SENT
          - DELIVERED
          - FAILED
          - UNDELIVERED
          - RECEIVED
        segment_count:
          type:
          - integer
          - 'null'
          description: 'Number of SMS parts the carrier split the body into (inbound: as received). Billing is per part.'
        mode:
          type: string
          enum:
          - LIVE
          - TEST
          description: 'TEST messages are simulated: never sent to a carrier and never billed.'
        is_verification:
          type: boolean
          description: The OTP SMS behind a verification. Its body is never stored.
        auto_reply_keyword:
          type:
          - string
          - 'null'
          enum:
          - STOP
          - START
          - HELP
          - null
          description: 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.
        cost_cents:
          type: integer
          description: 'What this message actually cost, in cents CAD: message_cost_cents times the parts 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_code:
          type:
          - string
          - 'null'
          description: '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.'
        error_message:
          type:
          - string
          - 'null'
          description: Human-readable reason for a FAILED or UNDELIVERED message, from the carrier when it gave one.
        sent_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        casl_consent_type:
          type:
          - string
          - 'null'
          enum:
          - EXPRESS
          - IMPLIED
          - null
          description: 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).
        dncl_exemption:
          type:
          - string
          - 'null'
          description: The CRTC Do Not Call List exemption the send relied on, when one applied.
        delivered_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When the carrier reported the message delivered to the handset.
      description: An SMS message, outbound or inbound. The same shape from the send, the lookup and the list.
    PaginationMeta:
      type: object
      additionalProperties: true
      required:
      - page
      - limit
      - total
      - pages
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        pages:
          type: integer
      description: Page-number pagination, returned as `meta` by the messages listing.
    Error:
      type: object
      description: Standard error body. The one visible exception is the OAuth 2.1 surface (/oauth/token, /oauth/register, /oauth/revoke), which follows RFC 6749/7591/7009 and answers { error, error_description } instead.
      required:
      - code
      - message
      - statusCode
      properties:
        code:
          type: string
          description: Machine-readable error code, e.g. VALIDATION_ERROR.
        message:
          type: string
          description: Human-readable message in the request's locale (Accept-Language).
        messageEn:
          type: string
          description: English message, always present alongside message.
        messageFr:
          type: string
          description: French message, always present alongside message.
        statusCode:
          type: integer
          description: The HTTP status, repeated in the body.
        details:
          description: 'Present on some errors: an AJV validation array, or error-specific structured detail.'
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: Use `mk_live_...` for production or `mk_test_...` for testing.
    apiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: 'Alternative to the Authorization header: send `mk_live_...` or `mk_test_...` as X-API-Key directly.'