Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.bloobank.com/llms.txt

Use this file to discover all available pages before exploring further.

A webhook consumer that “works on the happy path” is easy. A consumer that survives retries, duplicates, out-of-order events, partial failures, and timeouts is the actual goal. This page is a checklist of the patterns that get you there. For the delivery contract, see Retry & delivery. For the event catalog, see Payment events.

The non-negotiables

1. Verify the signature first

Before parsing, before logging, before anything. Untrusted input is a vulnerability. See Verifying signatures.

2. Deduplicate by messageId

Persist every messageId you have processed. On receipt, check first — if already seen, ack 200 without reprocessing.

3. Respond within 10 seconds

Hard timeout. Do real work asynchronously; ack first.

4. Use data.status / data.ordVersion as truth

Events may arrive out of order. Never trust the event name alone to infer current state.

The idempotent consumer pattern

Two patterns to internalize:

Pattern A — synchronous dedupe

If your work is fast (well under the 10-second budget), you can do it inline:
async function handleWebhook(req, res) {
  const event = verifySignature(req.rawBody, req.headers, SECRET);
  if (!event) return res.status(401).end();

  // Atomic insert — if it returns conflict, this is a duplicate
  try {
    await db.processedMessages.insertOne({ messageId: event.messageId, at: new Date() });
  } catch (e) {
    if (isDuplicateKey(e)) return res.status(200).end();   // already processed
    throw e;
  }

  await applyEvent(event);            // your business logic
  res.status(200).end();
}
The atomic insert is the dedupe gate. If the same messageId arrives twice concurrently, only one passes the insert; the other returns a duplicate-key error and acknowledges without processing. For anything that touches the database, calls an external service, or might be slow:
async function handleWebhook(req, res) {
  const event = verifySignature(req.rawBody, req.headers, SECRET);
  if (!event) return res.status(401).end();

  // Cheap dedupe check first (best-effort, race-tolerant)
  if (await alreadyProcessed(event.messageId)) {
    return res.status(200).end();
  }

  // Enqueue and ack immediately
  await queue.publish({ event });
  res.status(200).end();
}

// Worker:
async function processQueueItem({ event }) {
  // Authoritative dedupe via atomic insert
  try {
    await db.processedMessages.insertOne({ messageId: event.messageId, at: new Date() });
  } catch (e) {
    if (isDuplicateKey(e)) return;
    throw e;
  }
  await applyEvent(event);
}
The HTTP handler stays under 10 seconds even if downstream work is slow.

Handling out-of-order events

Events on the same order can arrive out of order. The defensive pattern uses the monotonic version field (data.ordVersion):
async function applyEvent(event) {
  const order = await db.orders.findOne({ id: event.data.id });
  if (order && order.lastSeenVersion >= event.data.ordVersion) {
    // Late event — older snapshot than we have. Ignore.
    return;
  }
  await db.orders.update(
    { id: event.data.id },
    {
      $set: {
        status:           event.data.status,
        lastSeenVersion:  event.data.ordVersion,
        lastUpdated:      event.occurredAt,
        // ... other fields you care about
      },
    },
    { upsert: true }
  );
  await fireBusinessLogic(event.data);
}
ordVersion increments on every mutation of the order’s content. Tracking the highest version you have seen makes your local view monotonically consistent, even when delivery order is scrambled.

When to GET vs. trust the payload

The webhook payload reflects the resource’s state at the moment of emission. For most uses, that is fresh enough — trust it. You should GET the resource fresh when:
  • You need fields that are not in the webhook payload (rare — data is the full resource).
  • A long delay elapsed between the event’s occurredAt and your processing time.
  • Your business logic depends on the absolute current state, not the state-at-emission.
GET is also useful for reconciliation: at daily intervals, list orders by updatedAt >= yesterday and verify each matches your local record.

Fast-ack patterns

The hard limit is 10 seconds. Strategies to stay well under:
StrategyWhen
Enqueue + ack (Pattern B above)Always preferable. Queue can be SQS, Kafka, Redis Streams, or in-process.
In-memory asyncLightweight tasks (e.g., update a single row). Risk: process crash loses the event before processing.
Inline processingOnly when work is provably bounded (e.g., a CPU-only computation taking < 100 ms).
Avoid:
  • HTTP calls to slow third parties (especially payment networks) inside the handler.
  • Synchronous email sends.
  • Database transactions that may deadlock.
Move all of these to a worker.

Replay-safety per event class

A retry can deliver the same event multiple times. Your business logic must be safe to apply more than once.
EventReplay-safe pattern
payment_order.createdUpsert into local order table. Second time updates nothing.
payment_order.successMark order as paid. Increment a settled-counter only on first observation; the dedupe table prevents double-counting.
payment_order.failedMark order as failed; alert operator on first observation.
payment_order.expiredMark order as expired; release any local hold.
The general rule: state changes are safe to retry because state is overwritten. Side effects (email, log entry, counter increment) must be gated behind the dedupe insert.

Observability

Track these per-event and aggregate:
MetricWhy
Webhook ack latency (p50, p99)Detect when you are approaching the 10-second timeout.
Webhook ack status code distributionIdentify endpoints returning 4xx/5xx.
Signature verification failures (count)Should be near zero; non-zero is a key drift or attempted abuse.
Duplicate messageId rateSome duplicates are expected (retries on transient receiver issues). A spike indicates BlooBank-side retries — usually correlates with downstream slowness.
Event-name distributionAnomalous spikes in payment_order.failed are operationally significant.
Worker queue depthBacklog growth means you cannot keep up — scale workers.
End-to-end lag (occurredAt → processed time)The user-visible “did my system see the event yet” metric.
Alert when:
  • Verification failure rate > 0 sustained for 5 minutes (key issue or attack).
  • Ack p99 > 3 seconds (heading toward timeout).
  • Queue depth grows monotonically for more than 10 minutes.

Common bugs (and their symptoms)

BugSymptom
Parsing the body before verifyingSignature mismatch on every delivery. Verify first, parse second.
Logging the raw signature headerSecret-adjacent material in logs. Log only the verification result.
Returning 400 on JSON parse errorPermanently failed delivery in BlooBank’s queue. Use 500 for any error you cannot classify.
Storing messageId in memory onlyAfter process restart, you reprocess everything. Persist to durable storage.
Slow inline workIntermittent timeouts and BlooBank retries. Move to a worker.
No timeout in your HTTP client (when you GET the resource in handler)Cascading slowness — your GET hangs, your ack misses 10 s, BlooBank retries, repeat. Bounded timeout (≤ 3 s) is mandatory.
Trusting event-name orderingRace conditions when events arrive out of order. Use ordVersion.
Reusing messageId for business idIf two unrelated events accidentally collide on a key your code derived from messageId, you may skip processing. messageId is for dedupe only.

Endpoint hardening

HardeningWhy
HTTPS onlyMandatory. The platform refuses HTTP.
Path obfuscationA receiver at /webhooks/bloobank-7a8b9c is harder to discover. Not a substitute for signature verification.
Rate limit your own endpointA misconfigured retry-storm should not overwhelm downstream. Bound concurrency to a known-safe level.
Restrict by IP if availableIf BlooBank publishes egress IP ranges (check the Dashboard), allowlist them at the edge. Compose with signature verification, do not replace it.

Next

Verifying signatures

Verify every delivery.

Retry & delivery

The delivery contract.

Payment events

Event catalog.