Skip to main content
Webhooks tell your server when something happens: a contact replies, a message is delivered or read, a message fails, someone reacts, or a contact is tagged. Blujay sends a signed POST with a JSON body to your HTTPS endpoint.

Set up an endpoint

1

Build a receiver

Your endpoint must:
  1. accept POST requests over public HTTPS;
  2. verify the signature using the raw request body;
  3. store the event (deduplicating by event_id) and return any 2xx within 10 seconds;
  4. do slow work afterwards, from your own queue.
2

Create a subscription

Response (201)
signing_secret is shown only once, in this response. Save it in your secret manager now. Later reads return only signing_secret_hint.
A subscription you create with the API starts 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.
3

Send a test message

There is no test event. Send a real message to your own phone with POST /v1/chats and reply to it. With the event list above you receive message.sent, then message.delivered on iMessage, then message.received when you reply.

The event payload

Every event has the same envelope. Event-specific fields are in data:

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_url values are relative API paths, such as /v1/attachments/att_abc123/content. Call them on https://api.blujay.io with your API key. Never send your key to a URL from any other host.
For a walkthrough of what arrives when a contact texts you, see Receive messages.

Select phone numbers

By default a subscription covers all your phone numbers, including ones you add later. To limit it to some numbers, set phone_number_filter:
Use 1 to 100 IDs from 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://. An http:// URL, or one with a username, password or #fragment, returns 400 WEBHOOK_URL_INVALID. A URL over 2048 characters fails validation with 400 VALIDATION_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, return 400 WEBHOOK_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 400 WEBHOOK_URL_UNRESOLVABLE. A DNS failure during delivery is retried.
  • Redirects. Blujay follows up to 3 redirects per attempt, sending the same POST with 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.
Your endpoint must respond within 10 seconds with any 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 400 FEATURE_NOT_ENABLED. See Reserved event types. To mirror opt-outs, poll opted_out on the contact (Opt-outs).
  • No contact_id on delivery events. message.sent, message.delivered, message.read and message.failed identify 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

  • Answer within 10 seconds. Store the event and return 2xx; do the real work from your own queue.
  • Verify the signature with the raw request body, byte for byte. A re-serialized body never matches.
  • Deduplicate by event_id. The same event can arrive more than once.
  • Never overwrite newer state with an older event. Compare sent_at, delivered_at and occurred_at before you update a record.
  • List subscriptions before you retry a create that timed out. Create is not idempotent.

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.