BeaconBox
Esc

Type to search.

Dashboard

Reading outcomes

The single most misread thing about this API. A push returns 201 even when the text was not sent, and that is the correct answer.

The shape of a response

Response · 201 Created
{
"id": "v55favd767qu",
"recipient_email": "buyer@example.com",
"subject": "Action needed on your order",
"kind": "one_off",
"status": "active",
"created": true,
"scheduled_for": null,
"nudged": true,
"email": {
  "sending": true,
  "skipped_reason": null
},
"sms": {
  "queued": false,
  "credits": 0,
  "skipped_reason": "insufficient_credit",
  "escalates_at": null
},
"sms_units": 0,
"whatsapp": null,
"whatsapp_opt_in": null,
"reference": "#A-10294",
"order_status": null,
"created_at": "2026-10-02T20:11:10.674762Z",
"updated_at": "2026-10-02T20:11:10.674762Z"
}

That response is a success. The update is in the inbox, the email is queued, and the text was not sent because the balance was empty.

Field Means
created true made a new update, false rewrote one in place.
nudged A nudge was queued. false on a quiet rewrite or notify: false.
scheduled_for When a deferred nudge will go. null means immediately, or not at all.
email Whether we expect to email, and why not if we do not.
sms, whatsapp The channel’s outcome, or null when the channel was not in play.
sms_units Carrier segments. Always 1 when one was queued, 0 otherwise.
reference, order_status Echoed from the push, null when it carried none. A status you do not recognise is a newer one: treat it as no status.

null on sms is not “skipped”. It means the account has that channel off and the request did not ask for it, so there is no outcome to report. An object with queued: false and a skipped_reason is a channel that was genuinely considered and refused.

Handling a skip

# Read .sms.skipped_reason, not the status code.
curl -s https://api.beaconbox.eu/api/v1/messages \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4711-payment-failed" \
-d '{"recipient_email": "buyer@example.com",
     "recipient_phone": "+37255550134",
     "subject": "Action needed on your order",
     "body": "We could not take payment.",
     "channels": ["sms"]}' \
| jq -r '.sms.skipped_reason // "queued"'

Neither SDK raises on a skip, deliberately.

What a skip can say

Reason Yours to fix?
no_phone Yes. Send a number, or set one on the recipient.
insufficient_credit Yes. Top up and resend.
country_not_allowed Usually. The destination is outside your allowed countries. Texts to the United States and Canada are not available yet, whatever your settings say: carriers there accept business texts only from a registered number. Nothing is charged.
phone_conflict Yes. The number belongs to another of your recipients.
disabled_by_request Yes. Your own channels excluded it.
sms_not_enabled, whatsapp_not_enabled Yes, by asking. The channel has not been switched on for your account yet; request it from the SMS or WhatsApp screen.
sms_disabled, whatsapp_disabled Yes. The channel is off for the account, including on a scheduled or armed send you switched the channel off after.
sms_warmup_cap_reached No, it passes. A newly enabled SMS account has a daily limit for its first two weeks. Decided when the send runs, so it appears on the message, not in the push response.
stopped No. They opted out of this channel. On SMS it holds until they text START.
unsubscribed No. They stopped all your updates. Permanent.
not_opted_in WhatsApp only. Collect consent first.
not_reachable No. Meta says the number is not on WhatsApp.
template_not_sendable WhatsApp only. Yours when the push did not carry both reference and order_status, or the reference or your business name cannot be shown: fix them (see order status); with sms_if_whatsapp_fails the text goes instead. Otherwise not yours: the template for that status is under review at Meta. Nothing is charged either way.
sending_paused No. Sending is paused on the account.
rate_limited, platform_limited No. Try later.

The email outcome, and why it has two halves

email on the push response is the gate’s verdict as known at push time, so the stable, recipient-shaped reasons reach you in the response to the call you just made rather than only through a webhook you may not have configured.

Means
sending: false Reliable. No email will be sent, and skipped_reason says why.
sending: true A strong expectation, not a promise.

The asymmetry is deliberate. At push time we can see a suppressed address, an unsubscribe or a lapsed plan. We cannot see the day’s cap filling up between now and the send, a pause applied in between, or a retraction. The nudge is queued and re-checked by the worker regardless.

skipped_reason here is one of sending_paused, business_inactive, plan_lapsed, unsubscribed, suppressed or platform_suppressed, and null when sending. business_inactive means the account has been deactivated. The last of those means an address another business already proved undeliverable, which gates the shared sending domain only.

So for what actually happened, read the message back.

curl https://api.beaconbox.eu/api/v1/messages/v55favd767qu \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
| jq '.delivery'
delivery
{
"delivered": false,
"delivered_at": null,
"opened": false,
"opened_at": null,
"bounced": false,
"bounced_at": null,
"sms": null,
"not_sent": {
  "reason": "plan_lapsed",
  "at": "2026-10-02T20:14:03.118221Z"
},
"failed": null
}

not_sent is what tells delivered: false apart from itself. Without it that flag meant both “on its way” and “never attempted, and never will be”, so a loop waiting for delivery had nothing to stop on. It is absent in the ordinary case, and a later successful send withdraws it. daily_cap_reached is final for that send: the nudge is not retried when the cap resets, so resend it the next day if it still matters.

failed is the other end of the same question: the email was attempted and will never be delivered. Its reason is esp_refused (the email provider rejected it outright), retries_exhausted (every attempt failed and BeaconBox stopped trying) or outcome_unknown (the provider stopped answering partway through the send). Either way, stop waiting. outcome_unknown means the email may in fact have arrived: BeaconBox will not send it a second time, because a duplicate would carry a second sign-in link, and if the provider later reports a delivery the message shows as delivered. Treat a reason you do not recognise as failed.

The paid channels follow the same rule. A text or WhatsApp message whose submission got no answer is never submitted again: it ends failed with the code sms.submit_outcome_unknown or whatsapp.submit_outcome_unknown, and its credit is kept, because the provider may have delivered it. The code arrives as error_code on the sms.failed or whatsapp.failed webhook, and both show in the dashboard’s send logs. A WhatsApp send first waits up to five minutes for Meta’s own status, and then falls back to a text if you asked for sms_if_whatsapp_fails. A message the provider refused on every attempt was never sent: it ends rejected with the code sms.submit_refused or whatsapp.submit_refused, its credit is refunded, and a WhatsApp one still falls back to a text when asked. One given up on before it ever reached the provider ends rejected and refunded the same way, with the code sms.never_submitted or whatsapp.never_submitted. Every text and WhatsApp message that ends undelivered sends its *.failed or *.rejected webhook once, whether the provider reported the outcome or BeaconBox decided it.

plan_lapsed is the one to alert on. Your pushes keep succeeding, because the updates are still reaching the inbox; it is only the email that has stopped, and there is no way to learn that from your own side. Subscribe to the plan.lapsed webhook.