Skip to Content
API access is available to organizations with a signed BAA. Request API access →
Webhooks

Webhooks

Your BloomText team turns this on for your account. Book a call and we’ll set it up with you.Request access

A webhook sends your server an HTTPS request when something happens in BloomText: a patient writes in, or a text your key sent is delivered or fails. You stop polling GET /patient-messages and react within seconds.

Webhooks carry IDs and a status, never message text. Your server fetches the details with the API, using the same key, so patient data only ever travels over an authenticated call.

Set up a webhook

Add the URL to a key

Each API key has at most one webhook. Under Settings → Account → API keys, an Owner or IT person opens the key and clicks Webhook, then sets:

  • the URL: https only, on a server you control. BloomText doesn’t follow redirects.
  • the events to send. Only events the key’s scopes allow are offered.

Store the signing secret

BloomText shows the webhook’s signing secret once, starting with whsec_. Store it with the key, in your secrets manager.

Send a test

Click Send test event. BloomText sends a webhook.test event, and the webhook’s page shows your server’s response.

Events

EventSent whenThe key needs
message.receivedA patient sends a message, on any channel, in an organization the key reaches. A key with a phone-number limit gets only messages to its numbers’ teams.messages.read
message.statusA text this key sent changes delivery status: sent, delivered, failed, undelivered or rejected. See Delivery status.messages.write
webhook.testYou click Send test event.—

The payload

Every event has the same envelope:

message.received
{ "id": "0f8e2a1c-6b4d-4f7e-9a2b-3c5d7e9f1a2b", "type": "message.received", "occurred_at": "2026-11-02T14:05:12Z", "org_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "data": { "message_id": "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "patient_id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77", "channel": "sms" } }
message.status
{ "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "type": "message.status", "occurred_at": "2026-11-02T14:06:40Z", "org_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "data": { "message_id": "7b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "patient_id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77", "status": "failed", "error_code": "30006" } }

id is the event’s ID and stays the same on every retry. To read the message, call Retrieve a patient message with message_id and org_id.

Check the signature

BloomText signs every request following Standard Webhooks . Each request has three headers:

HeaderValue
webhook-idThe event’s id
webhook-timestampWhen it was sent, in seconds since the Unix epoch
webhook-signaturev1, and the Base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the secret after whsec_, Base64-decoded. During a secret rotation it lists two signatures, separated by spaces.

Check the signature against the raw request body, before parsing it, and reject a timestamp more than 5 minutes from your clock.

verify.js
import crypto from 'node:crypto' export function verifyWebhook(headers, rawBody, secret) { const id = headers['webhook-id'] const timestamp = Number(headers['webhook-timestamp']) if (!id || !timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64') const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest() return headers['webhook-signature'].split(' ').some((part) => { const [version, signature] = part.split(',') const given = Buffer.from(signature ?? '', 'base64') return version === 'v1' && given.length === expected.length && crypto.timingSafeEqual(given, expected) }) }

Standard Webhooks libraries in most languages do the same check.

Delivery

  • Answer fast. Return any 2xx within 5 seconds. Do the work afterwards, for example from a queue.
  • Retries. Anything else, or no answer, is retried at 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours after the first try. 410 Gone stops retries for that event.
  • At least once, in any order. The same event can arrive twice, and events can arrive out of order. Skip an id you’ve already handled, and use occurred_at, or the message itself, for order.
  • No fixed addresses. Requests don’t come from a fixed set of IP addresses. Trust the signature, not the sender’s address.

If your server was down for longer than the retries cover, catch up by reading messages since the last event you handled.

Rotate the secret

On the key’s Webhook page, click Rotate secret. BloomText shows the new secret once. For the next 24 hours it signs every request with both the old and the new secret, so deploy the new one, then let the old one lapse.

Turn a webhook off

Remove the URL from the key, or revoke the key. Revoking a key stops its webhook at once.

Last updated on