Notify
Notify
Send one notification to one of your users and let HonkIO pick the channel: it tries push, SMS and email in order and stops at the first that delivers.
Quickstart
Notify sends one notification to one of your users and chooses the channel. You keep a recipient profile for each user, then call the API with the user's ID and the text. HonkIO tries the channels in order and stops at the first that delivers. Start with a test key: every channel runs in its own test mode.
- Save the person's contact details with
PUT /v1/recipients/:user_id: a phone number, an email address and a language, whichever you have. - Send with
POST /v1/notify: theuser_idand atext(and asubjectwhen an email may be tried), or atemplate. - Name your senders with
from.smsandfrom.email. A channel that could reach the person but has no sender is refused with 422NOTIFY_SENDER_REQUIRED. - Read the outcome from the 202 answer, from
GET /v1/notifications/:idor from thenotification.deliveredandnotification.failedwebhooks.
The contact details live in recipient profiles. A user with no profile can still be notified on push if they have an active device.
const { data: notification, error } = await honkio.notify.send({
userId: 'user_1042',
text: 'Your Acme code is 123456',
subject: 'Your Acme code',
from: { sms: '+1416XXXXXXX', email: 'hello@yourapp.ca' },
}, { idempotencyKey: 'login-code-1042-2026-11-03' })
if (error) throw new Error(error.message)
console.log(notification.id, notification.status, notification.channel)from honkio import Honkio
client = Honkio() # reads HONKIO_API_KEY
notification = client.notify.send(
user_id="user_1042",
text="Your Acme code is 123456",
subject="Your Acme code",
from_={"sms": "+1416XXXXXXX", "email": "hello@yourapp.ca"},
idempotency_key="login-code-1042-2026-11-03",
)
print(notification["id"], notification["status"], notification["channel"])<?php
use Honkio\Honkio;
$honkio = new Honkio(); // reads HONKIO_API_KEY
$notification = $honkio->notify->send([
'user_id' => 'user_1042',
'text' => 'Your Acme code is 123456',
'subject' => 'Your Acme code',
'from' => ['sms' => '+1416XXXXXXX', 'email' => 'hello@yourapp.ca'],
], 'login-code-1042-2026-11-03');
echo $notification['id'], ' ', $notification['status'], ' ', $notification['channel'], PHP_EOL;<?php
use Honkio\Laravel\Facades\Honkio;
// In a controller, a job or a listener:
$notification = Honkio::notify()->send([
'user_id' => (string) $user->id,
'text' => 'Your Acme code is 123456',
'subject' => 'Your Acme code',
'from' => ['sms' => '+1416XXXXXXX', 'email' => 'hello@yourapp.ca'],
], 'login-code-' . $user->id . '-2026-11-03');curl -X POST https://api.honkio.ca/v1/notify \
-H "Authorization: Bearer mk_test_YOUR_KEY" \
-H "Idempotency-Key: login-code-1042-2026-11-03" \
-H "Content-Type: application/json" \
-d '{
"user_id": "user_1042",
"text": "Your Acme code is 123456",
"subject": "Your Acme code",
"from": { "sms": "+1416XXXXXXX", "email": "hello@yourapp.ca" }
}'
# Response 202: { "id": "ntf_...", "status": "delivered", "channel": "sms", "attempts": [...], ... }Channels and order
By default Notify tries push, then SMS, then email, each at most once. Set channels to change the order or to leave a channel out.
A channel is skipped, with a reason on the attempt, when the person cannot be reached on it (no phone number, no email address, no active device), when it is off for the account or, in the default order, when the key may not send on it. A channel you name that the key may not send on is 403 FORBIDDEN. Contact points are read when you send: a channel the person could not be reached on then is skipped, even if a number, an address or a device is added before its turn.
An SMS or an email that its channel refuses falls through to the next channel, exactly where /v1/messages or /v1/emails would have refused it: no consent, an opt-out, a suppression, a sending limit, a pause or a low balance. A recipient profile is never consent.
text is the SMS, the push body and the email's plain text. subject and html refine the email. Or send a template with variables and each channel sends its own part, in the profile's language unless you pass language. See Templates.
const { data: notification, error } = await honkio.notify.send({
userId: 'user_1042',
template: 'order-shipped',
variables: { orderId: '5521' },
channels: ['sms', 'email'], // the order to try
from: { sms: '+1416XXXXXXX', email: 'hello@yourapp.ca' },
}, { idempotencyKey: 'order-5521-shipped' })
if (error) throw new Error(error.message)
for (const attempt of notification.attempts) console.log(attempt.channel, attempt.outcome, attempt.reason)from honkio import Honkio
client = Honkio() # reads HONKIO_API_KEY
notification = client.notify.send(
user_id="user_1042",
template="order-shipped",
variables={"orderId": "5521"},
channels=["sms", "email"], # the order to try
from_={"sms": "+1416XXXXXXX", "email": "hello@yourapp.ca"},
idempotency_key="order-5521-shipped",
)
for attempt in notification["attempts"]:
print(attempt["channel"], attempt["outcome"], attempt["reason"])<?php
use Honkio\Honkio;
$honkio = new Honkio(); // reads HONKIO_API_KEY
$notification = $honkio->notify->send([
'user_id' => 'user_1042',
'template' => 'order-shipped',
'variables' => ['orderId' => '5521'],
'channels' => ['sms', 'email'], // the order to try
'from' => ['sms' => '+1416XXXXXXX', 'email' => 'hello@yourapp.ca'],
], 'order-5521-shipped');
foreach ($notification['attempts'] as $attempt) {
echo $attempt['channel'], ' ', $attempt['outcome'], ' ', $attempt['reason'] ?? '', PHP_EOL;
}Notifications are for codes, reminders, receipts and alerts. is_commercial defaults to false. Set it only for an email that promotes something: it needs CASL consent on file and adds the unsubscribe footer. An SMS keeps its own consent rules.
What delivered means
A notification is delivered when a channel accepts it: a push that at least one device (a phone or a browser) accepted, an SMS the carrier accepted, or an email the email service accepted. It stops there.
A failure reported after that does not fall through to another channel: a delivery receipt that says the SMS did not arrive, a bounce or a complaint. Accepted is not the same as seen, and Notify never sends one notification twice to make up for it.
A push that no device accepts falls through after a wait of up to a minute, as a push with an SMS fallback does. The notification is pending while a push or an email is being sent. An SMS the carrier refuses, or an email the email service refuses at once, falls through, and what was charged for it is refunded.
A channel with no verdict for an hour fails the notification, with the reason NOTIFY_STUCK on that channel and notify_stuck on the channels skipped after it. An email still queued (a pause, the daily cap) is left to finish, and if it then fails no other channel is tried.
An SMS whose carrier call timed out counts as delivered: the carrier may have taken it, so no other channel is tried.
Send an Idempotency-Key header. The same key returns the first notification, pending or not, and never sends or charges twice. The same key with a different body is 409 IDEMPOTENCY_KEY_REUSED.
Every notification lists its attempts in order. Each has a channel, an outcome (delivered, pending, skipped, refused or failed) and a reason where there is one: one of the lowercase reasons below, or the channel's own error code, such as NO_CONSENT, OPT_OUT_BLOCKED or EMAIL_SUPPRESSED.
| no_active_device | The person has no active push device. |
| no_push_app | The account has no push app to send with. |
| no_phone_number | The profile has no phone number. |
| no_email | The profile has no email address. |
| no_sender | The channel had no sender to send from. |
| push_unavailable | The channel is off for the account. |
| push_disabled | The channel is off for the account. |
| email_unavailable | The channel is off for the account. |
| not_permitted | The key may not send on the channel. |
| template_channel_missing | The template has no part for the channel. |
| content_purged | The notification's text was cleared before this channel was tried. |
| NOTIFY_STUCK | A channel had no verdict for an hour. |
| notify_stuck | Skipped because an earlier channel was stuck. |
Pricing
Notify is included at no extra charge. A notification is billed at the delivering channel's normal price, and fall-through attempts aren't billed.
An SMS or email that was refused can still leave a failed message or email in your logs, and fire its own event, as the same send through /v1/messages or /v1/emails would.
Errors
Refusals come back as the usual error body. These codes belong to Notify; the others are in the error reference.
| HTTP | Code | Meaning |
|---|---|---|
| 501 | NOTIFY_COMING_SOON | Notify is not switched on for the platform yet. |
| 404 | RECIPIENT_NOT_FOUND | No profile with that user_id in the key's mode, and no active push device for it. |
| 422 | NOTIFY_SENDER_REQUIRED | A channel that can reach the person has no sender. details.channel names it. |
| 422 | NOTIFY_CONTENT_REQUIRED | Nothing to send: give text (and a subject to email), or a template. details.field names what is missing. |
| 422 | TEMPLATE_CHANNEL_MISSING | The template has no part for a channel you named, or for any channel that can be tried. |
| 422 | NOTIFY_CONTENT_TOO_LARGE | What the notification would send on the channels that can reach the person, rendered, is over 2 MB. details.bytes and details.limit give the sizes. |
| 409 | RECIPIENT_LIMIT_REACHED | The account already holds its maximum of profiles in this mode: 1,000,000 live, 1,000 test. details.limit gives it. |
| 409 | IDEMPOTENCY_KEY_REUSED | The Idempotency-Key was used for a different notification. |
| 403 | FORBIDDEN | The key lacks notifications write or recipients read, or may not send on any channel, or on one you named. |
Webhooks
notification.delivered fires when a channel delivered the notification and notification.failed when no channel did. Both carry the notification in data, with no message text.
The data is id, user_id, status, channel (the one that delivered), message_id, email_id, push_id (while push is on), attempts (each with channel, outcome and reason) and the livemode flag, as in the envelope.
The SMS, email or push behind a notification also fires its own events. Subscribe to the notification events when you want one answer per notification.
HonkIO