B Buffy Agent
Buffy Agent Blog · Engineering

Advanced Buffy webhook recipes — idempotency and reconciliation

Treat Buffy outbound webhooks as at-least-once delivery — idempotent handlers, correct HMAC verification on raw bodies, multi-tenant routing, and backfilling with GET /v1/events.

Advanced Buffy webhook recipes — idempotency and reconciliation

This post assumes you already completed outbound setup from Buffy webhook integrations: HTTPS endpoint, event subscriptions, X-Buffy-Signature secret, and a 200 within the timeout window.

Here we focus on production behavior: duplicates, signatures, tenancy, and catch-up when the network or your deploys misbehave.

At-least-once delivery, not exactly-once

Buffy retries failed outbound deliveries up to three times with exponential backoff (see the main guide). That implies:

  • Your handler may run more than once for the same logical event.
  • Timeouts are ambiguous — Buffy might have accepted your work but not received the 200 yet.

Design for idempotency: the second call should be a no-op or a safe update, not a second charge, duplicate CRM row, or duplicate notification blast.

Idempotent handlers

Pattern 1 — Natural idempotency
If your side effect is “set Stripe customer metadata to X,” repeated writes with the same value are harmless.

Pattern 2 — Dedupe key
Derive a stable key from the payload, for example:

  • user_id + event + activity.id + timestamp (when those fields exist in the envelope), or
  • SHA-256 of the raw body (works even when Buffy redelivers the identical JSON)

Store processed keys in Redis, Postgres, or your queue’s idempotency table with a TTL longer than your retry window.

import { createHash } from "node:crypto";

function dedupeKeyFromRawBody(raw: string): string {
  return createHash("sha256").update(raw, "utf8").digest("hex");
}

Reject (or short-circuit) duplicates after signature verification.

Verify X-Buffy-Signature on the raw body

The main guide states Buffy signs the payload with HMAC-SHA256 using your webhook secret. A common production bug is:

  1. Framework parses req.body as JSON.
  2. You stringify again for HMAC.
  3. Key ordering or whitespace differs → signature never matches.

Fix: In Express, use express.raw({ type: "application/json" }) on the webhook route (or verify in a layer that still has the raw buffer). In Fastify, use addContentTypeParser for application/json that keeps the buffer. Compute HMAC on those exact bytes, then parse JSON only after verification succeeds.

Pseudo-code:

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

function verifyBuffySignature(rawBody: Buffer, header: string | undefined, secret: string): boolean {
  if (!header) return false;
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const received = header.replace(/^sha256=/i, "").trim();
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(received, "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}

Use constant-time comparison (timingSafeEqual) to avoid leaking information about the secret.

Multi-tenant routing

Outbound envelopes include user_id. In B2B2C setups, map user_id to a tenant record before you enqueue work:

  • Partition background jobs by tenant (fair queues, per-customer rate limits).
  • Attach tenant context to observability (logs, traces) so one noisy customer does not drown others.

Avoid inferring tenant only from the URL path of your webhook endpoint unless you also validate it against the payload—the payload is authoritative.

Reconcile with GET /v1/events

Webhooks are best-effort; if all retries fail, the event may not reach your handler. Buffy also exposes a durable event log via GET /v1/events with cursor-based pagination (see Buffy webhook integrations).

Operational recipe:

  1. On deploy or after an outage, read the last stored cursor from your database.
  2. Page forward until you reach “now,” processing any missing IDs your webhook path never acknowledged.
  3. Store the new cursor.

This gives you auditability and recovery without giving up webhooks for the happy path.

When to prefer bulk APIs over inbound webhooks

For high-volume backfills, the main guide points at POST /v1/activities/bulk instead of hammering inbound webhooks. The same discipline applies on the outbound side: if you need every historical completion, export from the event log rather than expecting a perfect webhook stream across months.

Next step

If you have not generated types yet, add compile-time safety with TypeScript types from the Buffy OpenAPI spec, then keep the contract map handy in Buffy API reference overview.

Further reading