POST with a JSON body to your HTTPS endpoint.
Set up an endpoint
Build a receiver
Your endpoint must:
- accept
POSTrequests over public HTTPS; - verify the signature using the raw request body;
- store the event (deduplicating by
event_id) and return any2xxwithin 10 seconds; - do slow work afterwards, from your own queue.
Create a subscription
Response (201)
active unless you send "status": "disabled"; a subscription you create in Blujay under API → Webhooks starts switched off until you turn on delivery there. Blujay records an event only for subscriptions that are active when it happens, so nothing from a disabled period is stored, and nothing from it can be replayed later.The event payload
Every event has the same envelope. Event-specific fields are indata:
Handle events correctly
- Deduplicate by
event_id. Delivery is at least once, so you may receive the same event more than once. - Expect any order. Events can arrive out of order. Compare timestamps (
sent_at,delivered_at,occurred_at) and never overwrite newer state with an older event. - Reconcile when in doubt. The event body is a snapshot from when it happened. For the current state, read the resource, for example
GET /v1/messages/{message_id}. - Fetch media with your key. Media
download_urlvalues are relative API paths, such as/v1/attachments/att_abc123/content. Call them onhttps://api.blujay.iowith your API key. Never send your key to a URL from any other host.
Select phone numbers
By default a subscription covers all your phone numbers, including ones you add later. To limit it to some numbers, setphone_number_filter:
GET /v1/phone-numbers. A selection stays fixed when you add numbers. Contact events (contact.tag_added, contact.assigned) aren’t tied to a number and always cover the whole workspace.
Manage subscriptions
While a subscription is disabled, Blujay doesn’t record events for it. When you resume it, you receive only new events; the ones you missed are not delivered and can’t be replayed. A lost signing secret can’t be recovered: create a new subscription, switch your receiver to its secret, then delete the old one.
Creating a subscription doesn’t use an
Idempotency-Key. If a create request times out, list your subscriptions before trying again, so you don’t end up with two.
Where Blujay won’t deliver
Blujay checks the URL when you create or update a subscription, and resolves the hostname again before every delivery attempt.- Only
https://. Anhttp://URL, or one with a username, password or#fragment, returns400WEBHOOK_URL_INVALID. A URL over 2048 characters fails validation with400VALIDATION_ERROR. - Only public addresses.
localhost, Docker and cloud metadata hostnames, and any address in a loopback, link-local, private (10/8,172.16/12,192.168/16), carrier-grade NAT, multicast, documentation or reserved range, in IPv4 or IPv6, return400WEBHOOK_URL_UNSAFE. A hostname that later starts resolving to such an address fails the attempt with the same code. For local development, use a tunnel that gives you a public HTTPS URL. - The hostname must resolve. Otherwise creating the subscription returns
400WEBHOOK_URL_UNRESOLVABLE. A DNS failure during delivery is retried. - Redirects. Blujay follows up to 3 redirects per attempt, sending the same
POSTwith the body and signature headers to each address. Every address must pass the same checks. See Delivery and retries. - A valid certificate. Blujay verifies your TLS certificate the way a browser does. A self-signed, expired or mismatched certificate fails the attempt with
WEBHOOK_DELIVERY_NETWORK_ERROR, and the attempt is retried until you fix it.
2xx status. The response body is ignored.
What you don’t get
- No test event. Send a real message to yourself instead.
- No source IP range. Blujay doesn’t publish the addresses it sends from, so you can’t allowlist by IP. Trust the signature instead.
- No events for opt-outs, contact creation, updates or deletion, or chat creation. Those event types exist in the API schema but are reserved: subscribing to them returns
400FEATURE_NOT_ENABLED. See Reserved event types. To mirror opt-outs, pollopted_outon the contact (Opt-outs). - No
contact_idon delivery events.message.sent,message.delivered,message.readandmessage.failedidentify the chat, not the person. See Message lifecycle. - No events from a disabled period, and no replay of them.
- No secret rotation in place. To change the secret, create a new subscription and delete the old one.
Common mistakes
Next steps
Verify signatures
Confirm every request really came from Blujay.
Events
Every event type and its payload.
Delivery and retries
What happens when your endpoint is down.
Receive messages
What arrives when a contact texts you.