Base URL and auth
All webhook management endpoints live under:Endpoints
Create a webhook
Registers an HTTPS endpoint and returns its signing secret. The secret is shown in full once, in this response. Store it before you close the connection.Authorization: Bearer bluereacher_your_api_key. The webhook is created on the account tied to the key.
Request fields
Response fields
Example request
201 Created)
List webhooks
Returns every webhook on the account with rolling 24 hour delivery health, so you can see failures without reading your own logs.Authorization: Bearer bluereacher_your_api_key.
Request fields (query string)
Response fields
Example request
200 OK)
Update a webhook
Changes any part of a webhook, and rotates the signing secret when you ask for it. Send only the fields you want to change.Authorization: Bearer bluereacher_your_api_key.
Request fields
Response fields
Same object as
create-webhook, with the update applied. signing_secret appears only when rotate_secret was true.
Rotation is zero downtime. For 24 hours after a rotation, every delivery carries two signatures in one header, the new secret first:
v1 value matches. Deploy the new secret inside that window, then the old one stops being sent.
Example request
200 OK)
Delete a webhook
Removes a webhook permanently and drops any retries still queued for it. Deleted webhooks are not recoverable, and events that fire afterward are never replayed.Authorization: Bearer bluereacher_your_api_key.
Request fields
Response fields
Example request
200 OK)
To stop deliveries without losing the registration, set
status to paused with update-webhook instead.
Send a test delivery
Sends a signed sample event to the webhook’s URL right now and returns exactly what your server answered. Use it after every change to a URL, a secret, or a firewall rule.Authorization: Bearer bluereacher_your_api_key.
Request fields
Response fields
Test deliveries are signed the same way real ones are, carry
"test": true in the envelope, are never retried, and never count toward delivery health or the auto pause counter. Sample data uses msg_test_ and evt_test_ prefixes so you can filter it out.
Example request
200 OK)
200 OK on the API call. Check delivered, not the HTTP status of your test-webhook request.
Error notes
Event catalog
Subscribe to any subset, or pass["*"] for all of them.
Events fire once per state change. A single outbound message typically produces
message.sent, then message.delivered, then message.read, all carrying the same message id.
Payload reference
Every delivery uses the same envelope. Onlydata changes shape.
Envelope fields
Delivery headers
Message object
Used by everymessage.* event.
message.received
The inbound payload, in full. This is the exact body used in the worked verification example below.POST /send-message using to set to data.from and line_id set to data.line_id.
message.delivered
message.failed
message.fallback
channel is what the message went out on. requested_channel is what you asked for.
message.reaction
contact.opted_out
Opt outs are enforced across every line on your account. Suppress the contact in your own system when this arrives.line.status_changed
Your dedicated iMessage lines are managed for you. This event tells you when one changes state so you can pause or reroute sending in your own system.paused or suspended line returns line_unavailable on /send-message. Sending resumes on its own once the status returns to active, and you get another line.status_changed when it does.
campaign.completed
Signature verification
Every delivery is signed with HMAC-SHA256 using the webhook’s signing secret. Verify it on every request, before you parse or trust anything in the body. Steps- Read the raw request body as bytes, before any JSON parsing. The signature is computed over the exact bytes sent.
- Read
X-BlueReacher-Timestamp. Reject the request when it is more than 300 seconds away from your own clock, in either direction. - Build the signed string: the timestamp, a period, then the raw body.
"{timestamp}.{raw_body}". - Compute
HMAC-SHA256(signing_secret, signed_string)and hex encode it. - Compare against each
v1=value inX-BlueReacher-Signatureusing a constant time comparison. Accept when any one matches.
whsec_ prefix, with no decoding.
Node
Python
Worked example
Run these exact values through your code. If you get the same digest, your verification is correct. Signing secret:printf '%s' with no trailing newline. A trailing newline changes the bytes and changes the digest, which is the most common reason a hand check fails.
Endpoint requirements
Acknowledge first, process second. Write the payload to a queue, return 200, and do database writes, CRM calls, and AI replies outside the request. Slow handlers are the main cause of duplicate deliveries.
Retry policy
At-least-once delivery. An event can arrive more than once. Your handler has to be idempotent. This is the guarantee, not an edge case. A delivery counts as a success only when your server returns a 2xx within 10 seconds. Timeouts, connection resets, TLS failures, redirects, and any non 2xx status all count as failures. Backoff schedule
Each delay carries up to 20 percent random jitter so retries from a burst do not land together. The full window runs about 8 hours 45 minutes. After attempt 7 fails, the event is dropped and counted in
health.failed_24h. Dropped events are not replayed.
X-BlueReacher-Attempt tells you which attempt you are on. X-BlueReacher-Delivery-Id is new every attempt. The envelope id and data.id never change.
Auto pause. After 50 consecutive failures, the webhook’s status flips to paused and the account owner gets an email. Events that fire while a webhook is paused are not queued and are not replayed later. Fix your endpoint, confirm with /test-webhook, then set status back to active with /update-webhook.
Ordering is not guaranteed. A retried message.sent can land after message.delivered for the same message. Sort by data.created_at and treat message status as latest wins, keyed on data.id.
Deduplication
Dedupe on the message id plus the event name:id, which is also constant across attempts and works as a single column key everywhere.
Store the key with a 7 day TTL. The retry window closes in under 9 hours, so 7 days is a wide margin. On a repeat, return 200 immediately and skip the work. Never dedupe on X-BlueReacher-Delivery-Id, since it changes every attempt.
Errors
Management endpoints return this shape on every 4xx and 5xx:Troubleshooting
No deliveries arrive at all. Work through this in order: call/list-webhooks and check status is active; check events actually includes the event you expect; check line_id is not filtering out the traffic; check health.consecutive_failures and health.last_error. Then call /test-webhook. It reports the exact status code and error from your server.
Signatures never match. Almost always a raw body problem. Your framework parsed the JSON and re-serialized it, which changes key order or whitespace and breaks the digest. Capture the bytes before any body parser touches them: express.raw() in Node, request.get_data() in Flask, request.body before await request.json() in FastAPI. Other causes: comparing the full header value including the v1= prefix, whitespace or a newline copied into the secret, hashing the body alone instead of timestamp + "." + body, or a rotation in progress where the second v1 value is the one matching your deployed secret.
Signature checks fail with a stale timestamp. Your server clock has drifted past the 300 second tolerance. Run NTP. Never widen the tolerance past 300 seconds as a workaround, since that opens a replay window.
The same message arrives several times. Expected under at-least-once delivery, and it usually means your handler took longer than 10 seconds, so the attempt was recorded as failed and retried while your code was still working. Acknowledge with a 200 first, process afterward, and dedupe on data.id + ":" + event.
Deliveries stopped and the webhook shows paused. It hit 50 consecutive failures and auto paused. Check health.last_status_code and health.last_error for the cause. Events during the pause are gone and are not replayed, so backfill with /list-messages for the window before you resume.
Your server returns 403 or 406 and the payload never reaches your code. A WAF or proxy is filtering the request. Allow the BlueReacher-Webhooks/1 user agent, allow POST with application/json on that path, and email support@bluereacher.com for the current egress address list if you need an IP allowlist.
Events arrive out of order. Ordering is not guaranteed, especially after a retry. Sort by data.created_at and apply status as latest wins per data.id. Do not treat message.delivered arriving before message.sent as a bug.
message.read never fires. Read receipts are controlled by the recipient and are off by default for most people. Absence of message.read says nothing about delivery. Use message.delivered for that.
external_id is null on inbound messages. On message.received it is copied from the most recent outbound message to that contact on the same line. Cold inbound messages with no prior outbound have nothing to copy, so it comes through null. Match on contact_id or from in that case.
media_url returns 404 later. Inbound attachment URLs expire 24 hours after the event. Download the file inside your handler and store it yourself.
Local development gets invalid_webhook_url. Plain HTTP, localhost, and private addresses are rejected at create time. Use a tunneling service that terminates TLS on a public hostname, and point the webhook at that URL.
Rotating a secret without dropping events. Call /update-webhook with rotate_secret: true, deploy the new secret within 24 hours, and keep the multi value signature check shown above. Both signatures are sent during the overlap window, so no delivery fails while you deploy.