Skip to content
Bloomcount API
Esc
navigateopen⌘Jpreview
On this page

Webhooks

Subscribe to event changes, verify a delivery, and understand the retries.

A webhook subscription is a URL of yours that we POST to when something changes. Subscriptions are managed through the API itself, under the webhooks:manage scope.

Subscribing

curl -X POST "https://app.bloomcount.com/api/v1/webhooks" \
  -H "Authorization: Bearer bloomcount_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/bloomcount",
    "events": ["event.created", "event.updated"]
  }'

The response carries the subscription’s id and its signing secret:

{
  "id": "whsub_k97450a4nky6m96ex3fs5c5w7h8dt8bz",
  "secret": "whsec_..."
}

Event types

PropType
event.created?Event

An event was created, in the app or through the API.

TypeEvent
event.updated?Event

A scalar field on an event changed, such as its name, status, dates or budget.

TypeEvent

Line-level edits do not fire event.updated. Adding a product to an event, or changing a quantity, changes the event’s contents rather than the event record, and raises no delivery.

What the URL must be

The URL is checked when you subscribe, and a rejection returns one of the url_* codes from the errors page. It must use https:// on port 443, carry no username or password, and resolve to a public host: loopback, private ranges, .local, .internal and cloud metadata addresses are all refused. Redirects are not followed at delivery time either, so a public URL cannot bounce a delivery inward.

What a delivery looks like

Each delivery is a POST with a JSON body of three fields:

{
  "type": "event.created",
  "timestamp": 1788684000000,
  "data": {
    "eventId": "...",
    "slug": "smith-jones-wedding",
    "name": "Smith & Jones Wedding",
    "status": "quoting",
    "ceremonyAddress": "St Mary's Church, Bath",
    "venueAddress": "The Manor Hotel, Bath",
    "ceremonyStart": 1781424000000,
    "ceremonyEnd": 1781431200000,
    "venueStart": 1781430800000,
    "venueEnd": 1781467200000,
    "budget": 4500,
    "numberOfGuests": 80,
    "numberOfTables": 10,
    "colourScheme": ["blush", "ivory", "sage"]
  }
}

timestamp is milliseconds since the epoch. The payload carries the event’s scalar fields only. Heavier ones, and the event’s groups and lines, are left out deliberately: fetch GET /api/v1/events/{id} when you need them.

These headers come with it:

Header Value
X-Bloomcount-Event The event type, matching type in the body.
X-Bloomcount-Signature Hex HMAC-SHA256 of the raw body, keyed with your secret.
X-Bloomcount-Timestamp Seconds since the epoch, when the attempt was made.
Content-Type application/json

Verifying a delivery

Compute an HMAC-SHA256 of the raw request body, keyed with the secret, hex encode it, and compare it to X-Bloomcount-Signature with a constant-time comparison. Read the body as bytes rather than re-serialising a parsed object, or the bytes will not match.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(header ?? "", "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")

Reject anything that fails, and answer quickly: the delivery times out after ten seconds. If you have work to do, acknowledge first and do it afterwards.

Retries

Any 2xx counts as delivered. Anything else, including a timeout or a connection error, is retried on a fixed schedule:

Attempt When
1 Immediately
2 5 seconds later
3 30 seconds later
4 2 minutes later

After the fourth attempt the delivery is marked failed and the subscription’s failureCount goes up; a later success resets it to zero. Both that count and lastDeliveryAt are on GET /api/v1/webhooks, which is the quickest way to see whether your endpoint is healthy.

Deliveries are retried, so the same event can arrive more than once. Make your handler idempotent, keyed on the event id and timestamp.

Removing a subscription

curl -X DELETE "https://app.bloomcount.com/api/v1/webhooks/whsub_..." \
  -H "Authorization: Bearer bloomcount_..."

Was this page helpful?