BeaconBox
Esc

Type to search.

Dashboard

Webhooks

How delivery, opens and the things only we can see reach your system. Signed, retried, and deduplicated on an event id you have to store.

Delivery happens after your push returns, so it cannot be in the response. Webhooks are how it reaches you.

Registering an endpoint

curl https://api.beaconbox.eu/api/v1/webhook-endpoints \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/beaconbox"}'

An empty or omitted event_types means every event, and that is the recommended setting: a narrow subscription is how an event type added later silently passes your integration by. Branch on event.type in your handler and ignore what you do not recognise.

The url must be HTTPS and must resolve to a public address. Private, loopback and link-local addresses are refused, and redirects are never followed, because a public URL that redirects to an internal one is the cheapest way around a firewall.

The signing secret is returned once, at creation, and never again. Listing endpoints returns it masked.

The envelope

Every delivery is the same four fields, serialized with sorted keys and no whitespace.

Request body
{
"data": {
  "message_id": "v55favd767qu",
  "recipient_email": "buyer@example.com"
},
"id": "evt_9k2x4m7qd81a",
"occurred_at": "2026-10-02T20:19:44.903112+00:00",
"type": "message.bounced"
}

data carries what the event is about. Fields in it vary by type; message_id and recipient_email are on every message event.

Verifying a delivery

Every delivery carries three headers:

Header Carries
X-BeaconBox-Signature t=<unix>,v1=<hex>
X-BeaconBox-Event The event type, for routing before parsing
X-BeaconBox-Delivery The event id, the same value as id in the body

The signature is HMAC-SHA256 over "<t>.<raw body>" using your endpoint secret.

# Verifying by hand, for a language with no SDK. t and v1 come from the header.
# Split "t=1790000000,v1=abc..." into the two parts first.
T=$(printf '%s' "$SIG_HEADER" | sed 's/.*t=\([0-9]*\).*/\1/')
V1=$(printf '%s' "$SIG_HEADER" | sed 's/.*v1=\([0-9a-f]*\).*/\1/')
BODY=$(cat request-body.json)

EXPECTED=$(printf '%s.%s' "$T" "$BODY" \
| openssl dgst -sha256 -hmac "$BEACONBOX_WEBHOOK_SECRET" -r \
| cut -d' ' -f1)

# Real code compares in constant time and rejects a stale $T as well.
[ "$EXPECTED" = "$V1" ] && echo ok || echo reject

The helpers also check the timestamp, which is inside the signed string, so a captured delivery cannot be replayed later. They compare in constant time, and they raise WebhookVerificationError on a malformed or hostile signature rather than letting an unhandled exception out, so a single except really does answer 400 to everything.

Deduplicate on the event id

Deliveries are retried, so the same event.id can arrive more than once. A delivery that times out on your side may well have been processed.

Store handled ids and skip repeats. X-BeaconBox-Delivery carries that same id, so you can deduplicate from the header before parsing the body if you would rather. It is the event’s id and not the attempt’s: a retry of one event arrives with the id it had the first time, which is what makes it usable for this.

What you can subscribe to

Only outcomes you could not already know. There is no message.created: you just made that call and got the result.

Event Means
message.delivered A mailbox provider accepted the email.
message.opened The recipient followed the link and the inbox loaded.
message.bounced The address rejected it.
message.failed The send failed.
message.complained Marked as spam.
message.not_sent No email was attempted, with a reason.
sms.delivered, sms.failed, sms.rejected The text’s outcome.
whatsapp.delivered, whatsapp.failed, whatsapp.rejected The WhatsApp message’s outcome.
whatsapp.read They opened it. No email or SMS equivalent exists.
whatsapp.inbound_message They replied.
credits.low_balance The balance is running down.
plan.past_due A payment failed. The paid period still has time on it.
plan.lapsed Email sending has stopped.
plan.allowance_exceeded The included email allowance is used up.

The three to wire up before you need them

message.not_sent is the per-message half of the same story, and tells you an email was never attempted so you can stop waiting for delivery on it.

credits.low_balance is the one that stops a paid channel going quiet without anybody noticing.

Delivery behaviour

Your endpoint should answer 2xx quickly and do the work afterwards. A slow handler looks the same as a broken one.

Failed deliveries are retried. An event whose retries all fail counts once against the endpoint, and 20 of those in a row disable it instead of letting it be retried for ever. A single success resets the count.

Both the dashboard and the API tell you.

The webhook endpoints panel in Settings with no endpoints yet: a note that nothing is being notified, then the form to add one, with fields for the endpoint URL and a description and a note that selecting none receives every type above a grid of event checkboxes
The panel before an endpoint exists. Once one does, it lists each endpoint with its delivery state and a button to send a test ping.

GET /api/v1/webhook-endpoints returns the same thing to your own code: disabled, consecutive_failures, last_success_at and last_error for each endpoint. If deliveries stop arriving, that is what says why.

curl https://api.beaconbox.eu/api/v1/webhook-endpoints \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
| jq '.items[] | {url, disabled, consecutive_failures, last_error}'

Send nothing back in the body; it is not read.

Testing an endpoint

POST /api/v1/webhook-endpoints/{id}/test sends one sample ping event and reports what came back. There is a button for it in Settings too.

Two things make it safe to lean on. The event type is ping rather than a real one, so a handler that acts on message.* cannot mistake a test for a genuine delivery. And a test does not touch the endpoint’s health counters, so trying one against an endpoint you are repairing cannot be what disables it.

curl -X POST https://api.beaconbox.eu/api/v1/webhook-endpoints/wep_7k2x9m4qd1/test \
-H "Authorization: Bearer $BEACONBOX_API_KEY"