BeaconBox
Esc

Type to search.

Dashboard

Paid channels

SMS and WhatsApp cost a credit each and are refused rather than failed. How to ask for one, how to pay only for the people email missed, and what consent each needs.

Email is not optional, because it carries the link: there is no way to push an update that nudges on SMS alone. It can still be refused by the send path, which is what email on the response and delivery.not_sent on the message report. SMS and WhatsApp are additions to it, cost one credit each from one shared balance, and are refused, never raised, when they cannot send.

Asking for a channel

channels is declarative, not additive. A list names all the channels this push wants, so ["sms"] asks for SMS and excludes WhatsApp.

json
{
"recipient_email": "buyer@example.com",
"recipient_phone": "+37255550134",
"subject": "Action needed on your order",
"body": "We could not take payment. Tap to fix it.",
"channels": ["sms"]
}

Omit channels entirely to follow the account’s own settings, which is what most integrations should do: the shop owner then controls spend from the dashboard without a deploy.

Listing both sends two messages and costs two credits. There is no automatic fallback between them; sms_if_whatsapp_fails is how you ask for one explicitly.

Phone numbers

A number arrives either on the push, as recipient_phone, or on the recipient. Either way it is stored against that recipient for your business and reused, so it only needs sending once.

curl -X PUT https://api.beaconbox.eu/api/v1/recipients/buyer@example.com/phone \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone": "+37255550134"}'

E.164 is preferred. A number already held by another of your recipients is refused with phone_conflict and not moved, because the usual cause is a typo and the cost of guessing is texting the wrong person about somebody else’s order.

Paying only for the people email missed

escalate_if_unread_after_minutes holds the paid channel back. The text is sent only if the recipient still has not opened the update after that long. If they open it first, nothing is sent and nothing is charged.

curl 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. Tap to fix it.",
  "channels": ["sms"],
  "escalate_if_unread_after_minutes": 120
}'

Between 5 minutes and 7 days. It is ignored when the push sends no nudge, and when no paid channel was asked for.

The clock starts when the email does. escalates_at is send_at plus the window, or the push time plus the window when there is no send_at, so a scheduled update is never texted about before its email exists. If the email is still waiting to go when the deadline arrives (a backlog, a retry), the deadline moves to the same window after it.

Any outcome of the email counts as not read. An email that bounced, failed or was not sent (a suppressed address, a daily limit) escalates like one that was delivered and ignored: that recipient is exactly who the paid channel is for. The only message that never escalates is one for which no email was ever going to be sent.

Everything is decided again at the deadline, not at the push: whether they have read it, whether they still consent, whether the balance covers it, and whether the channel is still on for your account. A channel you switch off is not used by a deadline you armed last week. The skip shows on the message as sms_disabled or whatsapp_disabled, and POST /messages/{id}/sms is one call away if you change your mind.

On an account set to send a channel only on request, naming it in the push’s channels is the request, and the deadline keeps it: the channel you named is sent at the deadline as it would have been at the push. Switching the channel off, a country restriction and an opt-out still win.

For a shop sending a few thousand updates a month this is usually the difference between texting everybody and texting the fraction email did not reach.

WhatsApp needs an order and a status

A WhatsApp nudge is a template BeaconBox writes and Meta approves, one per order status: “Your order #A-10294 from PhonicBloom has shipped.” So a push goes out on WhatsApp only when it carries both reference and order_status, and both the reference and your business name can be shown (the display rules keep out anything a phone could turn into a link or a number to call). Missing either, or with a reference or a name outside those rules, WhatsApp reports template_not_sendable and nothing is charged; the email and any SMS go as usual. A name outside the rules stops WhatsApp for every push, and the WhatsApp screen will not let you switch the channel on until the name changes.

template_not_sendable also appears, through no fault of yours, while the template for that status is still in review at Meta. That one is ours to fix.

This is a change for integrations written before order statuses: their pushes stop going out on WhatsApp until they send order_status. With sms_if_whatsapp_fails, such a push is texted instead, once, the same way as for a number that is not on WhatsApp. So is an unread deadline, or a send_at send, whose update lost its status in an in-place update. A template still in review sends no text.

WhatsApp needs recorded consent before anything is sent, separate from SMS and from email, because Meta can audit it.

curl https://api.beaconbox.eu/api/v1/messages \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4711-dispatched" \
-d '{
  "recipient_email": "buyer@example.com",
  "recipient_phone": "+37255550134",
  "subject": "Your order is on its way",
  "body": "Collected by the courier.",
  "reference": "#A-10294",
  "order_status": "shipped",
  "channels": ["whatsapp"],
  "whatsapp_opt_in": {"source": "checkout tickbox"},
  "sms_if_whatsapp_fails": true
}'

Consent is per business. One you record is yours and reaches no other merchant. BeaconBox stamps the time: the push API cannot backdate, and an opt-out is permanent and cannot be reversed by recording consent again.

whatsapp_opt_in on the response reports what happened, and never fails the push. The one exception is a push with no number anywhere for the consent to be about, which is refused with 422 whatsapp.phone_required before anything is stored, so your idempotency key is not spent.

Falling back to SMS

sms_if_whatsapp_fails texts the recipient when the WhatsApp message proves undeliverable: Meta refuses it, the delivery status comes back failed, or no status arrives within five minutes.

It also texts at once when WhatsApp cannot carry the message at all: the number is known not to be on WhatsApp (not_reachable), or the push is template_not_sendable because it lacks reference or order_status, or the reference or your business name cannot be shown (see WhatsApp needs an order and a status). The same check runs at an unread deadline and at a send_at time, for an update whose status was cleared in place. It does not text while a status’s template is still in review at Meta. Either way it is one text at most.

It does not fire for a message that was delivered and not read. A read receipt is not delivery, and somebody who read it on another device would get the same update twice.

The text is a separate send with its own credit, under the same rules as any SMS: it counts as a request for the channel, so it goes on an account that sends SMS only on request, and not on one that has SMS off or not yet switched on. Read the message back to see it.

Resending one channel

After topping up, resend just the paid channel instead of pushing the whole update again.

curl -X POST https://api.beaconbox.eu/api/v1/messages/v55favd767qu/sms \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
-H "Idempotency-Key: 4a8c0e13-9b27-4f60-85d1-2e7fa36b0c94"

Refused with sms.already_sent if that update’s text is already queued, accepted or delivered, because sending again would only add a second charge and a second interruption. WhatsApp has its own whatsapp.already_sent, and whatsapp.escalated_to_sms for the case where the fallback text has already been delivered. An earlier send that ended without the provider’s answer may have arrived, so it is refused too: sms.may_have_sent and whatsapp.may_have_sent.

Credits

One balance covers both channels.

curl https://api.beaconbox.eu/api/v1/credits \
-H "Authorization: Bearer $BEACONBOX_API_KEY"
Response · 200 OK
{ "balance": 525, "low_balance": false, "threshold": 200 }

Buying is done in the dashboard, not over the API. Subscribe to credits.low_balance so a person does not have to be watching.