Channels API
A channel is a configured delivery endpoint owned by your workspace — an iOS app, an Android app, a web-push origin, an email sender, or an in-app surface. Each channel carries provider credentials (uploaded separately, see Credentials) and a verification status that reflects whether those credentials have been validated against the provider.
These endpoints are authenticated with a bearer token:
Authorization: Bearer YOUR_ACCESS_TOKEN
The channel object
| Field | Type | Description |
|---|---|---|
id | string | Channel UUID. |
type | string | One of ios, android, web_push, email, in_app, sms (Beta), whatsapp (Beta). |
name | string | Display name, unique per workspace. |
active | boolean | Whether the channel is enabled for sending. |
status | string | Credential verification state: pending, active, or invalid. |
verified_at | string | RFC 3339 timestamp of the last successful verification. Omitted until verified. |
verification_error | string | Provider error from the last failed verification. Omitted when there is none. |
created_at | string | Creation timestamp. |
A channel starts in status: "pending". Uploading credentials queues an
asynchronous verification against the provider; on success the channel moves
to active, on failure to invalid with verification_error populated.
Re-uploading credentials or calling the verify endpoint resets the cycle.
POST /api/v1/channels
Creates a channel.
Request Body
{
"type": "ios",
"name": "My iOS App"
}
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | One of ios, android, web_push, email, in_app. SMS and WhatsApp channels are provisioned as part of the Beta and cannot yet be created through this endpoint. |
name | string | Yes | Display name, up to 128 characters. |
Response — 201 Created
{
"id": "6f1d2a34-9c1b-4e8a-b0d2-1a2b3c4d5e6f",
"type": "ios",
"name": "My iOS App",
"active": true,
"status": "pending",
"created_at": "2026-07-27T12:00:00Z"
}
Error responses
| Status | Meaning |
|---|---|
401 | Missing or invalid bearer token |
403 | Channel type not included in your plan. The body carries "upgrade_required": true and an allowed list of the channel types your plan permits. |
422 | Validation failure (unknown type, missing or over-long name) |
GET /api/v1/channels
Lists all channels in the workspace.
Response — 200 OK
{
"channels": [
{
"id": "6f1d2a34-9c1b-4e8a-b0d2-1a2b3c4d5e6f",
"type": "ios",
"name": "My iOS App",
"active": true,
"status": "active",
"verified_at": "2026-07-27T12:05:00Z",
"created_at": "2026-07-27T12:00:00Z"
}
]
}
GET /api/v1/channels/:id
Fetches a single channel by UUID.
Returns 200 with the channel object, or 404 if no channel with that ID
exists in the workspace.
PATCH /api/v1/channels/:id
Updates a channel. Both fields are optional; omitted fields are unchanged.
Request Body
{
"name": "Renamed App",
"active": false
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | New display name. |
active | boolean | No | Enable or disable the channel for sending. |
Response — 200 OK
The updated channel object.
DELETE /api/v1/channels/:id
Deletes a channel.
Query parameters
| Param | Type | Description |
|---|---|---|
force | boolean | When true, deletes the channel even if it has credentials stored. |
Response
204 No Content on success.
| Status | Meaning |
|---|---|
404 | Channel not found |
409 | Channel has credentials stored and force=true was not supplied |
POST /api/v1/channels/:id/verify
Re-queues credential verification for the channel. Use this after fixing a provider-side problem (revoked key, changed bundle ID) without re-uploading the credentials.
Response — 202 Accepted
{ "status": "queued" }
Returns 404 if the channel has no credentials stored. Poll
GET /api/v1/channels/:id to observe the resulting status transition.
Credentials
Provider credentials are write-only secrets. They are encrypted with a
KMS-managed key at rest and are never returned by any API — the upload
response confirms only the credential's metadata (id, channel_id, type,
timestamps). If you lose a credential, upload a new one; there is no way to
read it back.
Uploading credentials to a channel that already has some replaces them and re-queues verification.
POST /api/v1/channels/:id/credentials/apns
Uploads an APNs token-signing key for an ios channel. The request is
multipart/form-data.
Form fields
| Field | Type | Required | Description |
|---|---|---|---|
key_file | file | Yes | The .p8 APNs auth key file from your Apple Developer account. |
key_id | string | No | The 10-character Key ID of the .p8 key. |
team_id | string | No | Your Apple Developer Team ID. |
bundle_id | string | No | The app's bundle identifier (used as the APNs topic). |
environment | string | No | sandbox or production. Defaults to sandbox. |
Response — 200 OK
{
"id": "cred-1",
"channel_id": "6f1d2a34-9c1b-4e8a-b0d2-1a2b3c4d5e6f",
"type": "apns",
"created_at": "2026-07-27T12:01:00Z",
"updated_at": "2026-07-27T12:01:00Z"
}
POST /api/v1/channels/:id/credentials/fcm
Uploads a Firebase service-account key for an android channel. The request
is multipart/form-data.
Form fields
| Field | Type | Required | Description |
|---|---|---|---|
key_file | file | Yes | The Firebase service-account JSON key file. |
Response — 200 OK
The credential confirmation object (same shape as APNs, with
"type": "fcm").
POST /api/v1/channels/:id/credentials/vapid
Generates a VAPID key pair server-side for a web_push channel. No request
body is required. The response includes the generated public key so you can
register it with the browser's push manager; the private key stays
encrypted server-side and is never exposed.
Response — 200 OK
{
"id": "cred-2",
"channel_id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"type": "vapid",
"vapid_public_key": "BODl3v8Yx...",
"created_at": "2026-07-27T12:01:00Z",
"updated_at": "2026-07-27T12:01:00Z"
}
POST /api/v1/channels/:id/credentials/sms
SMS is in Beta. SMS channels are provisioned by the Hober team during the Beta; contact support to enable the channel type on your workspace.
Uploads SMS provider credentials for an sms channel. JSON body.
Request Body
{
"api_key": "...",
"api_secret": "...",
"sender_id": "..."
}
| Field | Type | Required | Description |
|---|---|---|---|
api_key | string | Yes | Provider API key. |
api_secret | string | Yes | Provider API secret. |
sender_id | string | Yes | Registered sender ID or phone number. |
Response — 200 OK
The credential confirmation object with "type": "sms".
POST /api/v1/channels/:id/credentials/whatsapp
WhatsApp is in Beta. WhatsApp channels are provisioned by the Hober team during the Beta; contact support to enable the channel type on your workspace.
Uploads WhatsApp Business (Meta Cloud API) credentials for a whatsapp
channel. JSON body.
Request Body
{
"access_token": "...",
"phone_number_id": "...",
"waba_id": "...",
"app_secret": "...",
"webhook_verify_token": "..."
}
| Field | Type | Required | Description |
|---|---|---|---|
access_token | string | Yes | Meta system-user access token. |
phone_number_id | string | Yes | The WhatsApp Business phone number ID. |
waba_id | string | Yes | The WhatsApp Business Account ID. |
app_secret | string | No | Meta app secret — required only to receive inbound webhooks (opt-outs, template status callbacks). |
webhook_verify_token | string | No | Verify token for the Meta webhook subscription. |
Response — 200 OK
The credential confirmation object with "type": "whatsapp".
WhatsApp message templates
Part of the WhatsApp Beta — see above.
WhatsApp marketing messages must use a Meta-approved template. These endpoints manage the template lifecycle: draft locally, submit to Meta for review, then poll the approval status.
The template object
| Field | Type | Description |
|---|---|---|
id | string | Template UUID. |
channel_id | string | The owning whatsapp channel. |
name | string | Template name. |
language | string | Template language code. |
category | string | Meta template category. |
body | string | Template body text. |
status | string | Local approval state of the template. |
provider_template_id | string | Meta's template ID, present after submission. |
status_reason | string | Meta's rejection reason, when present. |
created_at | string | Creation timestamp. |
updated_at | string | Last-update timestamp. |
POST /api/v1/channels/:id/whatsapp-templates
Creates a template draft.
Request Body
{
"name": "order_shipped",
"language": "en_US",
"category": "MARKETING",
"body": "Your order is on the way!"
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Template name. |
body | string | Yes | Template body text. |
language | string | No | Language code. |
category | string | No | Meta template category. |
Returns 201 with the template object.
GET /api/v1/channels/:id/whatsapp-templates
Lists the channel's templates.
Response — 200 OK
{ "items": [ { "id": "...", "name": "order_shipped", "status": "..." } ] }
POST /api/v1/channels/:id/whatsapp-templates/:templateId/submit
Submits the draft to Meta for review. Returns 200 with the updated
template object, including the provider_template_id assigned by Meta.
POST /api/v1/channels/:id/whatsapp-templates/:templateId/refresh
Pulls the current approval status from Meta and applies it. Returns 200
with the updated template object — check status and, on rejection,
status_reason.