nohold / docs
Internals

Idempotency

How nohold prevents double-charges, double-sends, and double-writes when webhooks retry.

How nohold prevents double-charges, double-sends, and double-writes when webhooks retry. They will.

This is the section that matters most for evaluators. The TL;DR: every state change is gated by either a uniqueness constraint in the database or a single-claim guard. The probability of nohold doing the same thing twice in response to a retried webhook is, by design, zero under normal operation.

What can fire twice

  • Shopify retries orders/create (and other) webhooks when we return a non-2xx response.
  • Our internal dispatch queue retries failed outbound calls for some time after a failure.
  • Our own sweeper can re-enqueue work that started processing but never finished.

Without idempotency guards, these would lead to duplicate splits, duplicate Brightpearl Sales Orders, duplicate customer emails. We have one defense per fireable action.

Defense per action

Splitting an order

Shopify webhook deliveries are deduplicated by their event id at receipt time. A second delivery with the same id is silently ignored before any work runs. The split worker also single-claims the inbound row before doing any external API calls; a second invocation sees the claim and returns immediately.

Writing Brightpearl Sales Orders

Each Sales Order payload carries a reference field that Brightpearl treats as a uniqueness key. A retried dispatch of the same payload doesn't create a second SO; BP rejects the duplicate. If BP times out before responding, the next sweeper pass re-attempts; the BP-side reference makes the retry idempotent even if the original write succeeded silently before the timeout.

Sending customer notification emails

Every customer notification carries a per-event unique key, computed application-side from the split plus event type. The notification log is keyed on this. A retried send tries to insert a row with the same key, the database rejects it, and the second send becomes a no-op. The customer sees one email per logical event.

The email provider also dedupes on the same key as a defense in depth, and the queue layer has its own short-window dedupe. Three layers, any one of which is sufficient.

Releasing Shopify holds

Each fulfillment-hold release is single-claimed before the Shopify API call. If another worker already claimed it, we skip; if the Shopify call fails after the claim, the claim is cleared so the next webhook tick can retry.

Refund webhooks

Refund deliveries are deduped on the webhook id, so Shopify's intermittent re-delivery of the same refund event can't be counted twice. For partial refunds (multiple genuinely distinct refund events on the same order), each one increments the running total on the split. Running total, not double-counting.

What happens if it goes wrong anyway

If you ever see evidence of doubled state (two Brightpearl Sales Orders with identical content, two emails to the same customer for the same split, doubled financial figures) that's a bug. Email hello@nohold.app with the order number; we'll trace and fix.

In practice we monitor for these signals (duplicate BP SO references, notification_log rows clustered on the same split_order_id with the same notification_type) and the rate is zero across all merchants. The architecture is correct; the implementation is tested.

Where this matters most

For deposit pre-orders. If a merchant has 1,000 customers with $500 deposits and the system double-fires an ok to fulfill signal, the merchant is on the hook for $500K of inventory commitment. The Release only when stock arrives AND Shopify reports paid rule is built on the same idempotency model: the gate is checked once, the release fires once, and Shopify's payment state has to genuinely be paid (not just appear to be) before the release is allowed.

This is why nohold's payments boundary is we never touch payments. The financial state we read is Shopify's, audited by Shopify. We act on it; we never set it.

On this page