Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: HonkIO Contact Groups 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: Contact Groups
description: Group contacts and send group broadcasts
paths:
/v1/contact-groups:
get:
summary: List contact groups
tags:
- Contact Groups
description: Requires `contact_groups: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
in: query
name: search
required: false
description: Filter by group name (case-insensitive, partial match)
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: contact_groups:r
responses:
'200':
description: A page of groups, newest first, each with its members.
content:
application/json:
schema:
description: A page of groups, newest first, each with its members.
allOf:
- $ref: '#/components/schemas/PaginationOffset'
- type: object
additionalProperties: true
required:
- data
properties:
data:
type: array
items:
$ref: '#/components/schemas/ContactGroup'
'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: getV1ContactGroups
x-operation-id-source: derived
post:
summary: Create a contact group
tags:
- Contact Groups
description: Requires `contact_groups:w`.
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- name
properties:
name:
type: string
maxLength: 100
description:
type: string
maxLength: 500
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: contact_groups:w
responses:
'201':
description: The group.
content:
application/json:
schema:
$ref: '#/components/schemas/ContactGroup'
'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'
'409':
description: 'CONTACT_GROUP_ALREADY_EXISTS: the name is taken.'
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: postV1ContactGroups
x-operation-id-source: derived
/v1/contact-groups/{id}:
get:
summary: Get a contact group with its members
tags:
- Contact Groups
description: Requires `contact_groups:r`.
parameters:
- schema:
type: string
in: path
name: id
required: true
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: contact_groups:r
responses:
'200':
description: The group with its members.
content:
application/json:
schema:
$ref: '#/components/schemas/ContactGroup'
'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: CONTACT_GROUP_NOT_FOUND.
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: getV1ContactGroupsById
x-operation-id-source: derived
patch:
summary: Update a contact group
tags:
- Contact Groups
description: Requires `contact_groups:m`.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
maxLength: 100
description:
type: string
maxLength: 500
parameters:
- schema:
type: string
in: path
name: id
required: true
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: contact_groups:m
responses:
'200':
description: The updated group.
content:
application/json:
schema:
$ref: '#/components/schemas/ContactGroup'
'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: LIVE_KEY_REQUIRED when a test key tries to change something a live key's allow/deny list references. Also FORBIDDEN when the key lacks the permission.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: CONTACT_GROUP_NOT_FOUND.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: 'CONTACT_GROUP_ALREADY_EXISTS: the name is taken.'
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: patchV1ContactGroupsById
x-operation-id-source: derived
delete:
summary: Delete a contact group
tags:
- Contact Groups
description: Requires `contact_groups:d`.
parameters:
- schema:
type: string
in: path
name: id
required: true
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: contact_groups:d
responses:
'204':
description: Deleted. Its contacts are kept.
'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: LIVE_KEY_REQUIRED when a test key tries to change something a live key's allow/deny list references; KEY_FENCED when the change would loosen the calling key's own allow/deny fence. Also FORBIDDEN when the key lacks the permission.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: CONTACT_GROUP_NOT_FOUND.
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: deleteV1ContactGroupsById
x-operation-id-source: derived
/v1/contact-groups/{id}/members:
post:
summary: Add a member to a contact group
tags:
- Contact Groups
description: 'Provide either `contact_id` (existing Contact record) or `phone_number` (will be auto-created if not exists).
Requires `contact_groups:m`.'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
contact_id:
type: string
phone_number:
type: string
parameters:
- schema:
type: string
in: path
name: id
required: true
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: contact_groups:m
responses:
'201':
description: The member added.
content:
application/json:
schema:
description: The member added.
type: object
additionalProperties: true
required:
- id
- phone_number
- name
- added_at
properties:
id:
type: string
description: The contact id.
phone_number:
type: string
name:
type:
- string
- 'null'
added_at:
type: string
format: date-time
'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: LIVE_KEY_REQUIRED when a test key tries to change something a live key's allow/deny list references; KEY_FENCED when the change would loosen the calling key's own allow/deny fence. Also FORBIDDEN when the key lacks the permission.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: CONTACT_GROUP_NOT_FOUND or CONTACT_NOT_FOUND.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: CONTACT_GROUP_MEMBER_EXISTS.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: VALIDATION_ERROR (neither contact_id nor phone_number), INVALID_PHONE_NUMBER or CONTACT_HAS_NO_PHONE (the contact has no phone number).
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: postV1ContactGroupsByIdMembers
x-operation-id-source: derived
/v1/contact-groups/{id}/members/{contactId}:
delete:
summary: Remove a member from a contact group
tags:
- Contact Groups
description: Requires `contact_groups:m`.
parameters:
- schema:
type: string
in: path
name: id
required: true
- schema:
type: string
in: path
name: contactId
required: true
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: contact_groups:m
responses:
'204':
description: Removed from the group. The contact is kept.
'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: LIVE_KEY_REQUIRED when a test key tries to change something a live key's allow/deny list references; KEY_FENCED when the change would loosen the calling key's own allow/deny fence. Also FORBIDDEN when the key lacks the permission.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: CONTACT_GROUP_NOT_FOUND or CONTACT_GROUP_MEMBER_NOT_FOUND.
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: deleteV1ContactGroupsByIdMembersByContactId
x-operation-id-source: derived
/v1/contact-groups/{id}/messages:
post:
summary: Send an SMS to all members of a contact group
tags:
- Contact Groups
description: 'Always answers 207, whether every member sent or every member failed: check each entry in `results` rather than the HTTP status. Each result names the member `to` number. A live broadcast repeated with the same Idempotency-Key within 24 hours answers with the first attempt''s stored 207, without running the whole-group checks again or using another daily broadcast slot. A later retry with that key runs the checks again (and can use a slot) but never re-messages a member the first attempt already reached.
Requires `messages:w`.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- from
- body
properties:
from:
type: string
description: Sending number (E.164, must be owned by account)
body:
type: string
maxLength: 1600
description: Up to 1600 characters and 10 SMS parts. Longer bodies are rejected with 422 MESSAGE_TOO_LONG before any member is messaged.
skip_consent_check:
type: boolean
parameters:
- schema:
type: string
in: path
name: id
required: true
- schema:
type: string
maxLength: 128
in: header
name: idempotency-key
required: false
security:
- apiKey: []
- apiKeyHeader: []
x-honkio-permission: messages:w
responses:
'207':
description: 'Always 207: check each result, not the status. A live broadcast repeated with the same Idempotency-Key within 24 hours answers with the first attempt''s stored result.'
content:
application/json:
schema:
description: 'Always 207: check each result, not the status. A live broadcast repeated with the same Idempotency-Key within 24 hours answers with the first attempt''s stored result.'
type: object
additionalProperties: true
required:
- group_id
- sent_to
- failed
- results
properties:
group_id:
type: string
sent_to:
type: integer
description: Members not failed.
failed:
type: integer
results:
type: array
items:
type: object
additionalProperties: true
required:
- to
- status
properties:
to:
type: string
description: The member's number.
status:
type: string
description: 'Lowercase: the message status (queued, sent, delivered and so on), or failed.'
message_id:
type: string
description: Absent when the member failed before a message was created.
error:
type: string
description: 'An error code: why the member failed, or the message''s own error_code.'
estimated_completion_at:
type:
- string
- 'null'
format: date-time
description: An estimate of when the carrier will have sent the last member, from the sending number's queue. Null in test mode. Absent from a stored replay of a broadcast sent before it existed.
'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, SENDING_PAUSED, or TOLL_FREE_NOT_VERIFIED (a toll-free from number without an approved toll-free verification; test keys too). A live member without a confirmed double opt-in fails alone in the results with DOUBLE_OPT_IN_REQUIRED.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: CONTACT_GROUP_NOT_FOUND.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: VALIDATION_ERROR, BROADCAST_TOO_LARGE, MESSAGE_TOO_LONG or LINK_SHORTENER_BLOCKED.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: BROADCAST_LIMIT_REACHED, DAILY_LIMIT_REACHED, FANOUT_LIMIT_REACHED, NUMBER_QUEUE_FULL (the broadcast's message parts would overfill the sending number's carrier queue; Retry-After is set when waiting would make room) or RECIPIENT_RATE_LIMITED, or RATE_LIMITED at 100 requests per second.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Unexpected server error.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: postV1ContactGroupsByIdMessages
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.
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.'
ContactGroup:
type: object
additionalProperties: true
required:
- id
- name
- description
- member_count
- members
- created_at
- updated_at
properties:
id:
type: string
name:
type: string
description:
type:
- string
- 'null'
member_count:
type: integer
members:
type: array
items:
type: object
additionalProperties: true
required:
- id
- phone_number
- name
properties:
id:
type: string
description: The contact id.
phone_number:
type:
- string
- 'null'
name:
type:
- string
- 'null'
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
description: A contact group with every member embedded.
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.'