BeaconBox
Esc

Type to search.

Dashboard

Pushing updates

The one operation most integrations need. What each field does, and how to make the second push about an order rewrite the first instead of joining it.

POST /api/v1/messages writes an update to a customer’s inbox and queues a nudge about it. It requires an Idempotency-Key.

The body

Field Required What it does
recipient_email yes Who it is for. Creates the recipient if you have not messaged them before. The response repeats it as stored: trimmed and lower case.
subject yes Up to 300 characters. Also the matching key for updateable.
body yes Plain text and clickable links. No HTML. Up to 10,000 characters.
kind no one_off (default) or updateable.
reference no Your order number, up to 32 characters.
order_status no What happened to the order: confirmed, shipped, ready_for_pickup, delivered, cancelled, refunded or delayed. Chooses the wording of the WhatsApp and SMS nudges. See order status.
notify no Force or suppress the nudge.
recipient_phone no Stored against the recipient and reused on later pushes.
channels no Which nudge channels this push wants. On an account set to send a channel only on request, naming it is the request, and an escalation keeps it.
send_at no Hold the nudge until a moment, up to 90 days out.
escalate_if_unread_after_minutes no Hold the paid channel back. See paid channels.
obsoletes no Ids of your earlier updates this one replaces.
whatsapp_opt_in no Record consent as part of the push.
sms_if_whatsapp_fails no Text them if WhatsApp proves undeliverable, or cannot carry this push at all. See paid channels.

Order status

order_status says what happened to the order this update is about. BeaconBox writes the WhatsApp and SMS nudges from it, in its own words: your subject and body never appear in either, because a nudge is a notice to come and read the update, and the update is yours.

order_status The nudge says
confirmed Your order #A-10294 from PhonicBloom is confirmed.
shipped Your order #A-10294 from PhonicBloom has shipped.
ready_for_pickup Your order #A-10294 from PhonicBloom is ready for pickup.
delivered Your order #A-10294 from PhonicBloom was delivered.
cancelled Your order #A-10294 from PhonicBloom was cancelled.
refunded Your order #A-10294 from PhonicBloom has been refunded.
delayed Your order #A-10294 from PhonicBloom is delayed.

WhatsApp needs both reference and order_status. Meta accepts a business notification only when it says what happened to which order, so a push missing either skips WhatsApp with template_not_sendable, and nothing is charged. Email is unaffected, and with sms_if_whatsapp_fails the text goes at once instead (see paid channels).

A nudge shows the reference exactly as you sent it, or not at all. It may start with one #, then a letter or digit, followed only by letters, digits, _, /, # and -, at most 32 characters. A dot is allowed only directly before a digit, and at most twice (INV.2026 and 2026.001 are fine, shop.com is not). Long runs of digits and leading zeros are fine. A reference the push accepts but that falls outside this is stored and echoed back but never shown: such a push sends no WhatsApp message, and its text leaves the order number out.

Your business name is held to a similar rule. It may use the letters A to Z, these accented letters (Ä Å Æ Ç É Ñ Ö Ø Ü ß à ä å æ è é ì ñ ò ö ø ù ü), digits, single spaces and & ' ’ - . , ( ), at most 40 characters. It starts with a letter or digit and ends with a letter, a digit, . or ). A dot only ends a word (it is followed by a space, ), , or the end of the name) or sits in a run of initials such as S.A. or e.V.. Brackets must pair, and the name has no www, @, : or / and fewer than six digits in all. A name outside that sends no WhatsApp message at all (the WhatsApp screen says so, and WhatsApp cannot be switched on until it changes), and the SMS leaves it out. The SMS shows a name only when it is plain ASCII, so Café 24 appears on WhatsApp but not in the text.

SMS uses the same words when they fit one text. A text is one credit because it is one segment of 160 GSM-7 characters, so BeaconBox sends the first of these that fits:

The text says
1 Your order #A-10294 from PhonicBloom has shipped: (link)
2 Your order #A-10294 has shipped: (link)
3 Your order has shipped: (link)
4 You have a new order update: (link)

Without a status the text is the last one, as it always has been. When the account has an opt-out number, each form carries a last line, “Reply STOP to (number) to opt out”, and the generic one reads “New order update: (link)”. That line counts toward the 160 characters, so with it a long reference or business name is the first thing left out. Either way it is one text and one credit. The email nudge is unchanged: it already carries your subject.

An updateable push replaces the status and the reference along with the body, so the update that says delivered nudges as delivered. A rewrite without order_status clears it: the next nudge about that update has no status wording and no WhatsApp message. More statuses may be added: the response echoes the value as a plain string, so treat one you do not recognise as no status.

One-off or updateable

This is the decision that shapes the integration.

one_off always creates a new update, and nudges unless you set notify: false. Right for things that are genuinely separate events: a receipt, a refund confirmation.

updateable is matched on (recipient, subject). If no live update exists with that subject, it creates one and nudges. If one does, it rewrites that update in place and, by default, says nothing.

So the useful pattern for an order is one subject per thread of meaning, pushed repeatedly:

# Three pushes, one subject, one update in the customer's inbox.
for STEP in "confirmed|Packed and waiting for collection." \
          "shipped|Collected by the courier. Tracking XY123456789EE." \
          "delivered|Delivered and signed for at 14:20."; do
STATUS="${STEP%%|*}"
BODY="${STEP#*|}"
curl https://api.beaconbox.eu/api/v1/messages \
  -H "Authorization: Bearer $BEACONBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-4711-$STATUS" \
  -d "{\"recipient_email\": \"buyer@example.com\",
       \"subject\": \"Your order is on its way\",
       \"body\": \"$BODY\",
       \"reference\": \"#4711\",
       \"order_status\": \"$STATUS\",
       \"kind\": \"updateable\"}"
done

The customer is nudged once, on the first push, and then holds an update that stays current.

Only one live updateable update can exist per (business, recipient, subject). Superseding or retracting one frees that slot.

Controlling the nudge

notify overrides the default:

kind Subject exists notify unset notify: true notify: false
one_off not checked nudge nudge silent
updateable no, creates nudge nudge silent
updateable yes, rewrites silent nudge silent

The row that matters is the last one. A rewrite is quiet by default, which is right for a tracking number that changed. When the revision is one they need now, send notify: true.

notify: false on a first push is for an update you want readable without interrupting anybody: somewhere to point a customer who asks, rather than something to announce.

Scheduling

send_at holds the nudge until a moment you name. RFC 3339 with a timezone, in the future, at most 90 days out.

json
{
"recipient_email": "buyer@example.com",
"subject": "Your collection slot is tomorrow",
"body": "Tomorrow between 09:00 and 11:00. Reply to the email to change it.",
"send_at": "2026-10-03T07:00:00Z"
}

The update is readable immediately. Only the nudge waits, because the inbox is the source of truth and the nudge is what should land at a civilised hour.

It is ignored when the push sends no nudge: a notify: false push, or a quiet rewrite.

With escalate_if_unread_after_minutes, the unread window counts from send_at, not from the push, so a scheduled update is never texted about before its email has gone. escalates_at on the response says when.

A scheduled nudge is decided when it goes out, not when you push it. Credits are charged then, so a balance that drains overnight turns a scheduled text into a skip you can retry; and switching SMS or WhatsApp off calls off anything still waiting on that channel, however far ahead it was scheduled. To call off one update’s nudges on every channel at once, retract it (below).

Replacing earlier updates

obsoletes takes ids of your own earlier updates and greys them out in the customer’s inbox.

json
{
"recipient_email": "buyer@example.com",
"subject": "Your refund has been issued",
"body": "£42.00 is on its way back to the card you paid with.",
"reference": "#A-10294",
"order_status": "refunded",
"obsoletes": ["v55favd767qu"]
}

Use it when a new update makes an old one wrong rather than out of date. The delivery estimate for an order that has since been cancelled should not still read as live.

Taking one back

POST /api/v1/messages/{id}/retract withdraws an update, cancels any queued nudge, and refunds nothing because nothing was charged for a send that had not happened.

curl -X POST https://api.beaconbox.eu/api/v1/messages/v55favd767qu/retract \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
-H "Idempotency-Key: 2c9e5b71-0a44-4d9f-8e21-6b3fd0cc9a55"

Read already_notified. It is the difference between “nobody ever knew” and “they were told, and now they have been told it is withdrawn”.

Retracting something already withdrawn succeeds with retracted: false. That is not a failure, it is the operation being idempotent about its own meaning.

Listing and reading back

GET /api/v1/messages is cursor paginated and filterable by recipient. Both SDKs expose it as a generator so a year of history is not held in memory.

curl "https://api.beaconbox.eu/api/v1/messages?recipient_email=buyer@example.com&limit=50" \
-H "Authorization: Bearer $BEACONBOX_API_KEY"

For more than a handful of pushes at a time, see Batches.