Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: HonkIO Verify 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: Verify
description: Phone number verification (SMS OTP) with per-verification upcharge
paths:
/v1/verify:
get:
summary: List verifications
tags:
- Verify
description: 'Returns a paginated list of verifications for the authenticated account, newest first.
Requires `verify:r`.'
parameters:
- schema:
type: integer
minimum: 1
maximum: 100
default: 50
in: query
name: limit
required: false
- schema:
type: integer
minimum: 0
default: 0
in: query
name: offset
required: false
- schema:
type: string
enum:
- pending
- verified
- expired
- max_attempts
in: query
name: status
required: false
description: Filter by status
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: verify:r
responses:
'200':
description: A page of verifications, newest first.
content:
application/json:
schema:
description: A page of verifications, newest first.
allOf:
- $ref: '#/components/schemas/PaginationOffset'
- type: object
additionalProperties: true
required:
- data
properties:
data:
type: array
items:
$ref: '#/components/schemas/Verification'
'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: getV1Verify
x-operation-id-source: derived
post:
summary: Start a phone number verification
tags:
- Verify
description: 'Sends an OTP to the specified Canadian phone number.
Billed at your standard per-part message rate (the OTP is one part unless `app_name` is long or uses non-GSM characters; the charge is settled to the carrier''s part count) plus a per-verification upcharge: see `verification_upcharge_cents` from `GET /v1/pricing`. A verification the carrier rejects is refunded in full.
**Options:**
- `code_length`: digits in the OTP, `4`, `6` (default), or `8`
- `ttl_minutes`: how long the code is valid (1 to 60 min, default 10)
- `app_name`: brand name shown in the SMS, e.g. `"Acme"` → *"Your Acme verification code is: …"*
In test mode (`mk_test_...` key) no SMS is sent; the code is always all zeros for the chosen length (e.g. `000000` for 6-digit, `0000` for 4-digit). `from` may be any valid NANP number in test mode, so an account with no number yet can try it; a toll-free `from` must still be yours and verified.
Requires `verify:w`.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- from
- to
properties:
from:
type: string
description: Your HonkIO number (E.164, must be active on your account). A test key may use any valid NANP number, except a toll-free one, which must be yours and verified in both modes.
to:
type: string
description: The phone number to verify (E.164, Canadian numbers only)
code_length:
type: integer
enum:
- 4
- 6
- 8
description: 'OTP digit length (default: 6)'
ttl_minutes:
type: integer
minimum: 1
maximum: 60
description: 'Minutes until the code expires (default: 10)'
app_name:
type: string
maxLength: 64
description: 'Brand name shown in the SMS body (default: HonkIO)'
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: verify:w
responses:
'201':
description: 'Verification started: the code was sent (LIVE) or simulated (TEST, code all zeros).'
content:
application/json:
schema:
$ref: '#/components/schemas/Verification'
'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: 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'
'422':
description: VALIDATION_ERROR, NON_CANADIAN_NUMBER, RESERVED_DESTINATION, UNDELIVERABLE_NUMBER, NOT_A_MOBILE_NUMBER or LINK_SHORTENER_BLOCKED. A refused start costs nothing.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: RATE_LIMITED (one start per recipient per 60 seconds, Retry-After 60), DAILY_LIMIT_REACHED, FANOUT_LIMIT_REACHED, NUMBER_RATE_LIMITED or RECIPIENT_RATE_LIMITED.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'451':
description: 'OPT_OUT_BLOCKED: the recipient opted out.'
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 SMS. The 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 code. Nothing was charged; start again after Retry-After. CARRIER_TIMEOUT: the carrier did not answer in time, so the code may or may not have reached the recipient. The verification is closed, so that code can never be checked, and the charge is refunded: start a new one.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: postV1Verify
x-operation-id-source: derived
/v1/verify/{id}:
get:
summary: Get a verification
tags:
- Verify
description: 'Returns a single verification record by ID.
Requires `verify:r`.'
parameters:
- schema:
type: string
in: path
name: id
required: true
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: verify:r
responses:
'200':
description: The verification.
content:
application/json:
schema:
$ref: '#/components/schemas/Verification'
'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: VERIFICATION_NOT_FOUND. A test key finds TEST verifications 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: getV1VerifyById
x-operation-id-source: derived
/v1/verify/{id}/check:
post:
summary: Check a verification code
tags:
- Verify
description: 'Submit the OTP code the end-user received. Max 5 attempts. Accepts 4, 6, or 8 digit codes depending on how the verification was started.
Requires `verify:w`.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- code
properties:
code:
type: string
description: 4 to 8 digit OTP code
parameters:
- schema:
type: string
in: path
name: id
required: true
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: verify:w
responses:
'200':
description: 'Code accepted: the verification is now verified.'
content:
application/json:
schema:
description: 'Code accepted: the verification is now verified.'
allOf:
- $ref: '#/components/schemas/Verification'
- type: object
additionalProperties: true
required:
- attempts_remaining
properties:
attempts_remaining:
type: integer
description: Always 0 on success.
'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: VERIFICATION_NOT_FOUND. A test key finds TEST verifications only.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: VERIFICATION_ALREADY_VERIFIED.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'410':
description: 'VERIFICATION_EXPIRED: start a new verification.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: VALIDATION_ERROR (not 4 to 8 digits) or VERIFICATION_INVALID_CODE, which carries attempts_remaining.
content:
application/json:
schema:
description: VALIDATION_ERROR (not 4 to 8 digits) or VERIFICATION_INVALID_CODE, which carries attempts_remaining.
allOf:
- $ref: '#/components/schemas/Error'
- type: object
additionalProperties: true
properties:
attempts_remaining:
type: integer
description: Wrong-code attempts left. Present only on VERIFICATION_INVALID_CODE.
'429':
description: 'VERIFICATION_MAX_ATTEMPTS: five wrong codes; start a new verification. Also RATE_LIMITED at 100 requests per second.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: 'INTERNAL_ERROR: unexpected server error.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: postV1VerifyByIdCheck
x-operation-id-source: derived
components:
schemas:
PaginationOffset:
type: object
additionalProperties: true
required:
- total
- limit
- offset
properties:
total:
type: integer
description: Total matching records.
limit:
type: integer
offset:
type: integer
description: Offset pagination, returned alongside `data` by the verify, contacts, contact groups and lists listings.
Verification:
type: object
additionalProperties: true
required:
- id
- phone_number
- from_number
- status
- attempts
- code_length
- app_name
- mode
- cost_cents
- expires_at
- verified_at
- created_at
properties:
id:
type: string
phone_number:
type: string
from_number:
type: string
status:
type: string
enum:
- pending
- verified
- expired
- max_attempts
attempts:
type: integer
code_length:
type: integer
app_name:
type:
- string
- 'null'
mode:
type: string
enum:
- LIVE
- TEST
description: 'TEST verifications are simulated: no SMS is sent, nothing is billed, and the code is always 000000 padded to code_length.'
cost_cents:
type: integer
description: 'Cost in cents CAD. On a TEST verification this is 0: nothing was charged.'
expires_at:
type: string
format: date-time
verified_at:
type:
- string
- 'null'
format: date-time
created_at:
type: string
format: date-time
description: A phone-number verification (SMS OTP). status is lowercase, unlike message statuses.
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.'