Skip to content

Webhook — delivery and ordering guarantees

Confirmed against backend code (merchants/consumers.py):

  • Synchronous within the request that changes the state. The payment_merchant signal fires inside Payment.update(), and the handler runs in the same thread as the call.
  • Single attemptsession.send(request) with no timeout= or retry loop.
  • If the merchant returns HTTP > 204 or is down, the webhook is lost (logged as an error on the backend side; no retry).
async function handleWebhook(body) {
const existing = await db.orders.findByB4bitId(body.identifier);
if (existing && new Date(body.edited_at) <= new Date(existing.last_update)) {
// webhook viejo, ya tenemos info más reciente.
logger.debug('Webhook obsoleto, ignorando', { identifier: body.identifier });
return 200;
}
await db.orders.upsertStatus({
b4bit_id: body.identifier,
status: body.status,
last_update: body.edited_at,
raw: body,
});
return 200;
}
  1. Nightly reconciliation: a job that runs every early morning and calls GET /orders/?start=YESTERDAY&end=TODAY&page=1&items_per_page=100, iterates every page, and cross-checks against your database. For every payment whose edited_at advanced, update.
  2. Point-in-time query: if you detect an “orphan” order (exists in your checkout but never received a webhook), fetch GET /orders/info/{identifier} for the authoritative state.
  3. Endpoint high availability: reverse proxy with at least 2 workers, healthcheck, downtime alerts.
  4. Async response: immediate HTTP 200 + internal queue (Redis, SQS, Celery) to process heavy logic without blocking the ACK.

Your handler must tolerate the same (identifier, status) arriving more than once:

  • Use identifier + edited_at as the unique dedupe key.
  • Do not run side effects (sending emails, updating inventory) more than once per state.
  • Persist the fact that “I already processed this state” before running the side effect.