BLOMBoundary Layer Labs
All guides

DocsDevelopers

Webhooks

BLOM tells a receiver you run when a task, a next action, a claim, or a closure changes. Each delivery is signed and carries no content.

In BLOMOrganizationConnect AgentWebhooks

What a webhook sends

A webhook is a signed POST that BLOM sends to an https address you run whenever something changes in your organization: a task captured, moved, or closed, a next action, a claim, a closure, or token use recorded. It carries identifiers, state names, and times, never content such as a title, a brief, or a note. It changes nothing in BLOM. Your receiver reads the details it needs through the public API.

Only an Administrator manages webhooks: a person with that role, or an agent's key with Admin access. Anyone else sees "Only an Administrator of this organization manages its webhooks and sees their deliveries." Each webhook has an owner, whoever added it, and is sent events only while its owner is an active Administrator.

Add a webhook

  1. Open Webhooks

    Open Organization, then Connect Agent, and select Webhooks in its Webhooks section.

  2. Name the receiver

    Select Add webhook. In Receiver URL, give an https address on a public domain name, such as https://hooks.example.com/blom, on the standard port, with no user name, password, or # part. BLOM sends to no IP address or local name. Add a Description (optional); left empty, it's the receiver's host.

  3. Choose its events and product

    Under Events, choose All events, which takes every type, including any type added to the catalog later, or Chosen events and select the ones you want. Under Product, keep All products, or choose one product to be sent only its events.

  4. Store the signing secret

    Select Add the webhook. BLOM shows the webhook's signing secret once. Select Copy the secret, put it where your receiver reads it, and then select I've stored it.

  5. Send a test

    Open the webhook from the list and select Send test. The test's delivery shows under Deliveries in the last 30 days, with each attempt's result.

Caution. Keep the secret out of repositories, tasks, and documents. If it's lost or exposed, rotate it.

How many webhooks an organization may have depends on its plan: see Pricing. From a terminal, the CLI adds one and prints its secret once. Its --url is the receiver's address, so BLOM's own comes from BLOM_URL or your sign-in:

node blom-cli.mjs webhooks create --url https://hooks.example.com/blom --events task.closed,claim.started

Events

TypeIn BLOMSent whenIts own fields
task.capturedTask capturedA task is captured or opened.homeItemId
task.phase_changedTask movedA task moves to another phase.fromPhaseId, toPhaseId, toPhaseLabel
task.closedTask closedA task closes.resolution: completed, duplicate, declined, or cancelled
task.reopenedTask reopenedA closed task reopens.None
next_action.changedNext action setA task gets a new next action.nextActionId, kind, and waitsOn: whom it waits on
next_action.completedNext action completedA next action is completed.nextActionId
claim.startedClaim madeA person or an agent claims a next action.claimId, nextActionId, holderActorId, state
claim.endedClaim endedA claim ends.claimId, nextActionId, endKind, such as completed or taken_over
closure.returnedClosure returnedA session's closure is returned for a task.closureId, claimId, status
token_use.recordedToken use recordedA token-use report is recorded.tokenUseId, source
webhook.testTestYou select Send test.None

Every event's data holds organizationId, endpointId, and eventId. The task events, the first nine, also hold changeId, the actor who made the change (its kind, person, agent, or service, and its id), taskId, taskKey, and productId. token_use.recorded holds changeId, then taskId, taskKey, and productId, which are null when the report names no task or product. It names no actor. Besides ids, states, and times, the only text an event carries is a phase's label, in toPhaseLabel.

The request

Each delivery is a POST with these headers and no others:

HeaderValue
content-typeapplication/json
user-agentBLOM-Webhooks/1
webhook-idThe event's id, as in the body's eventId, the same on every attempt. A webhook-id you've seen before is a repeat.
webhook-timestampWhen this attempt started, in Unix seconds.
webhook-signaturev1, and a signature. For 24 hours after a rotation, two of them, separated by a space.

The body is JSON with the event's type, the timestamp when the change was recorded, and its data. BLOM sends it on one line; it's spread out here to read:

{
      "type": "task.closed",
      "timestamp": "2026-11-02T14:05:09.120Z",
      "data": {
        "organizationId": "…",
        "endpointId": "…",
        "eventId": "…",
        "changeId": "…",
        "actor": { "kind": "agent", "id": "…" },
        "taskId": "…",
        "taskKey": "HL-12",
        "productId": "…",
        "resolution": "completed"
      }
    }

BLOM builds the body the same way on every attempt, so a retry or a redelivery carries the same bytes under the same webhook-id.

Verify the signature

BLOM signs each delivery as Standard Webhooks does, so a Standard Webhooks library checks it with your secret. To check it yourself:

  1. Keep the body as received

    Check the body's exact bytes, before you parse it. Parsing the JSON and writing it out again changes the bytes, and the signature with them.

  2. Check the timestamp

    Refuse a delivery whose webhook-timestamp is more than 5 minutes from your clock, as the libraries do.

  3. Make the key

    Drop whsec_ from the start of the secret and decode the rest from base64. That gives the 32-byte key.

  4. Sign the content

    Join the webhook-id, the webhook-timestamp, and the body with periods, as id.timestamp.body. Compute its HMAC-SHA256 with the key, and encode the result in base64.

  5. Compare

    Split webhook-signature on spaces. Each part is v1, followed by a signature. The delivery is BLOM's when any one of them equals yours, compared in constant time.

In Node.js, for example:

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

    // secret: the webhook's signing secret, whsec_…
    // headers: the request's headers, by lowercase name
    // body: the request's body as text, exactly as received
    export function isFromBlom(secret, headers, body) {
      const id = headers["webhook-id"] ?? "";
      const timestamp = headers["webhook-timestamp"] ?? "";
      const age = Math.abs(Date.now() / 1000 - Number(timestamp));
      if (!(age <= 300)) return false;
      const key = Buffer.from(secret.slice("whsec_".length), "base64");
      const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).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 && timingSafeEqual(given, expected);
      });
    }

Then answer with a 2xx status within 10 seconds, and do the work after.

Retries and failures

A delivery succeeds when your receiver answers with a 2xx status within 10 seconds. Anything else is a failed attempt: another status, no answer in time, a redirect, which BLOM doesn't follow, or an address BLOM refuses to send to when it connects.

After a failed attempt, BLOM tries again 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, and 12 hours later. The seventh failure, about 21 hours after the first, holds the delivery as Failed after 7 attempts. A test gets one attempt.

  • BLOM sends a webhook's deliveries one at a time, oldest first. After any failed attempt, it sends that webhook nothing for 60 seconds.
  • 50 failures in a row pause the webhook: Paused after 50 failed deliveries in a row.
  • A 410 Gone pauses it at once: Paused: the receiver answered 410 Gone.

A webhook's page lists its deliveries of the last 30 days, newest first, 50 at a time. Open one to see each attempt's time, result, status, error, and duration, and the body sent. Redeliver sends it again now, under the same webhook-id, with a new series of attempts.

Pause, rotate, or delete

Pause
BLOM sends the webhook nothing, tests included, until you select Resume. What it was owed shows as Not sent: the endpoint was paused, and resuming doesn't send it. Redeliver any of it you still want.
Rotate secret
BLOM makes a new secret and shows it once. For 24 hours each delivery carries two signatures, the new secret's first, then the old one's, so you can move your receiver over. After that, it carries the new one alone.
Delete
BLOM sends nothing more and cancels what it still owed. Its record is kept, but it no longer shows on Webhooks.
Edit settings
Change the receiver URL, the description, the events, or the product, then select Save the settings.

If a webhook's owner stops being an Administrator, BLOM pauses it and says so. Resuming it makes you its owner.

From a terminal, the CLI's webhooks pause, resume, rotate-secret, delete, test, and deliveries each take the webhook's id, which webhooks list shows. Its op command reaches the rest, such as webhooks.updateEndpoint and webhooks.redeliver.

Can't find what you need?

Write to us, and we'll answer and add it here.

service@boundarylayerlabs.com