BioFlow / API docs / Webhooks

Webhooks that hold up in production.

BioFlow POSTs signed events to your endpoint when things happen in a workspace — a lead captured, a page published, a sale paid or refunded. Signing follows Standard Webhooks v1, retries ride an exponential ladder, and everything is manageable over the API: create, test, rotate, resend, replay.

Events

Four event types (plus a test event).

Every delivery is a JSON envelope { id, type, created_at, data }. The id (whmsg_…) is stable across retries — delivery is at-least-once, so use it as your dedup key.

contact.created

A contact/lead is captured: newsletter signup, form submission, or gated download.

{ "id": "whmsg_evt_abc123", "type": "contact.created", "created_at": "2026-07-20T12:00:00.000Z", "data": { "contact": { "id": "ct_abc123", "email": "[email protected]", "name": "Alex Fan", "source": "NEWSLETTER", "source_block_id": "blk_abc123", "created_at": "2026-07-20T12:00:00.000Z" } } }

page.published

A bio page goes live — an immediate publish or a scheduled one firing.

{ "id": "whmsg_evt_def456", "type": "page.published", "created_at": "2026-07-20T12:00:00.000Z", "data": { "page": { "id": "pg_abc123", "title": "My links", "slug": "maya", "published_at": "2026-07-20T12:00:00.000Z" } } }

sale.paid

A tip or digital-product sale completes (Stripe checkout succeeded).

{ "id": "whmsg_evt_ghi789", "type": "sale.paid", "created_at": "2026-07-20T12:00:00.000Z", "data": { "sale": { "id": "sl_abc123", "kind": "PRODUCT", "gross_amount_cents": 1500, "net_amount_cents": 1425, "currency": "usd", "customer_email": "[email protected]", "created_at": "2026-07-20T12:00:00.000Z" } } }

sale.refunded

A sale is refunded (Stripe charge.refunded). Same sale object as sale.paid.

{ "id": "whmsg_evt_jkl012", "type": "sale.refunded", "created_at": "2026-07-20T12:00:00.000Z", "data": { "sale": { "id": "sl_abc123", "kind": "PRODUCT", "gross_amount_cents": 1500, "net_amount_cents": 1425, "currency": "usd", "customer_email": "[email protected]", "created_at": "2026-07-20T12:00:00.000Z" } } }

endpoint.test

Sent synchronously at endpoint creation, on URL change, and on POST …/test — never persisted, bypasses your event-type filters.

{ "id": "whmsg_test_9b1c...", "type": "endpoint.test", "created_at": "2026-07-20T12:00:00.000Z", "data": { "message": "BioFlow webhook verification — reply 2xx to confirm this endpoint." } }

Subscribing

Create an endpoint with the event types you want — POST /v1/webhook-endpoints or the developer console. Creation sends a signed endpoint.test event synchronously; your endpoint must answer 2xx or creation fails with endpoint_verification_failed. The signing secret (whsec_…) is shown once, on creation.

Signing

Standard Webhooks v1 signatures.

The wire contract

Three headers ride every delivery:

webhook-id: whmsg_evt_abc123 webhook-timestamp: 1784548800 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pCPT...

The signature is HMAC-SHA256 over `${id}.${timestamp}.${rawBody}`, keyed with the base64-decoded bytes after the whsec_ prefix, base64-encoded. During secret rotation the header carries two space-separated v1, entries — verifying with either secret succeeds. Timestamp tolerance is 300 seconds.

Verify with the SDK

// SDK source: https://github.com/DevinoSolutions/bioflow-sdk // (npm package @getbioflow/sdk is landing shortly — build from source today) import { verifyWebhook } from "@getbioflow/sdk"; // IMPORTANT: pass the RAW request body — verify first, JSON.parse never. export async function POST(request: Request) { const rawBody = await request.text(); const event = verifyWebhook({ payload: rawBody, headers: request.headers, secret: process.env.BIOFLOW_WEBHOOK_SECRET!, // whsec_... }); // throws WebhookVerificationError on ANY failure switch (event.type) { case "contact.created": console.log("new contact", event.data.contact.email); break; case "sale.paid": console.log("sale", event.data.sale.gross_amount_cents); break; } return new Response("ok"); }

Verify with Node.js crypto (no dependency)

import { createHmac, timingSafeEqual } from "node:crypto"; /** Standard Webhooks v1 — verify BEFORE parsing, over the raw bytes. */ function verifyBioFlowWebhook(input) { const { id, timestamp, signatureHeader, rawBody, secret } = input; // 1. Reject stale timestamps (300s tolerance). if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 2. HMAC-SHA256 over "id.timestamp.rawBody", keyed with the // base64-DECODED bytes after the whsec_ prefix. const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key) .update(`${id}.${timestamp}.${rawBody}`, "utf8") .digest("base64"); // 3. The header holds space-separated "v1,<base64>" entries (two during // secret rotation) — constant-time compare against each. return signatureHeader.split(" ").some((entry) => { const [version, signature] = entry.split(","); if (version !== "v1" || !signature) return false; const a = Buffer.from(signature); const b = Buffer.from(expected); return a.length === b.length && timingSafeEqual(a, b); }); }

Verify over the RAW request bytes — parsing and re-serializing the JSON before verifying will break the signature.

Reliability

Retries, the failure ladder, and replay.

Retry schedule

Up to 8 attempts per delivery — only a 2xx response counts as delivered. Delay after each failed attempt (±20% jitter):

  • attempt 1 fails → retry 30 seconds later
  • attempt 2 fails → retry 2 minutes later
  • attempt 3 fails → retry 10 minutes later
  • attempt 4 fails → retry 1 hour later
  • attempt 5 fails → retry 4 hours later
  • attempt 6 fails → retry 12 hours later
  • attempt 7 fails → retry 24 hours later

Auto-disable at 20 consecutive failures

20 consecutive failed deliveries disable the endpoint and notify the workspace owner. Fix your endpoint, re-enable it with PATCH /v1/webhook-endpoints/{endpoint_id} {"enabled": true}, then bring back what you missed:

  • POST …/replay — re-enqueue failed deliveries in bulk
  • POST …/deliveries/{delivery_id}/resend — restart the ladder for one delivery
  • POST …/rotate-secret — rotate whsec_ with a dual-signature overlap window
Reference

The 10 webhook operations.

BioFlow webhook-endpoint management operations
OperationEndpoint
List webhook endpointsGET /v1/webhook-endpoints
Create a webhook endpointPOST /v1/webhook-endpoints
Delete a webhook endpointDELETE /v1/webhook-endpoints/{endpoint_id}
Get a webhook endpointGET /v1/webhook-endpoints/{endpoint_id}
Update a webhook endpointPATCH /v1/webhook-endpoints/{endpoint_id}
List deliveriesGET /v1/webhook-endpoints/{endpoint_id}/deliveries
Resend a deliveryPOST /v1/webhook-endpoints/{endpoint_id}/deliveries/{delivery_id}/resend
Replay failed deliveriesPOST /v1/webhook-endpoints/{endpoint_id}/replay
Rotate the signing secretPOST /v1/webhook-endpoints/{endpoint_id}/rotate-secret
Send a test eventPOST /v1/webhook-endpoints/{endpoint_id}/test

Webhook scopes are webhooks:read and webhooks:write. Endpoint URLs must be public HTTPS — private-network targets are rejected. Back to the API docs hub.