One payment, two bookings: stop duplicate automation

A retry is normal when systems talk to each other. Your workflow must recognise the same business action before a second delivery turns into a second booking or message.

By DLVX · 5 min read · Published 20 October 2026

Read the worked guideCheck the sources

One payment, two bookings: stop duplicate automation

  1. Receive

    Authenticate the event and save durable work.

  2. Claim

    Atomically claim the business action key.

  3. Act

    Use destination idempotency where supported.

  4. Confirm or reconcile

    Save the receipt; uncertain outcomes wait for evidence.

A teaching sequence. This diagram does not run an AI tool or connect to your business.

One enquiry arrives twice. What should happen?

Send the same fictional event twice, then reset and try a missing receipt.

    Local teaching simulation. No real booking, API call or stored customer data. A real implementation needs durable state and an actual provider lookup.

    On this page

    A timeout does not mean nothing happened

    Imagine a fictional Bangkok workshop selling places online. A customer pays once. The payment event reaches an automation, which reserves a seat and sends a confirmation. Before the automation acknowledges the event, its connection drops. The payment provider delivers the event again; a second seat and confirmation appear.

    The model did not need to hallucinate for this to happen. The failure lives in the action sequence. Asking the model to remember whether it already booked the customer is not a reliable repair, especially when two workers process the same event at the same time.

    Stripe documents duplicate webhook delivery and does not guarantee event ordering. Other providers have their own delivery rules, which you should check. Stripe webhook guidance, checked 20 October 2026. Design your workflow around the possibility that an event arrives again.

    Track the event and the business action separately

    An event ID answers whether you received that exact event before. A business action ID answers whether you already performed the intended effect. Both matter. Different events can refer to the same purchase, and someone can manually rerun a job with a new execution ID.

    For the workshop, a useful action key might combine your own order ID with the operation: order-842:reserve-seat. A separate key, order-842:send-confirmation-v1, represents the initial confirmation. Do not use the model's generated wording as the key. A new greeting does not make a second confirmation a different business action.

    Scope keys to the correct account and operation. Two shops can have the same local order number. A refund is not the same action as a reservation, and a deliberate amended confirmation needs its own explicit identity.

    Keep enough history to cover the business's possible replay window. A provider's request-key retention period does not automatically cover a manual replay next month. Your own ledger owns that longer-lived decision.

    Checking a spreadsheet first is not enough

    Two workers can both read an empty status and both decide to act. The check and the claim must happen atomically, normally through a database constraint or transaction. A unique action key lets one worker claim the operation while the other sees that it already exists.

    Use clear states such as pending, in progress, confirmed, failed and unknown. Confirmed means you have evidence of the effect. Unknown means the request may have succeeded but you do not yet know. These states prevent a timeout from becoming permission to repeat the action.

    For a database-owned seat allocation, reserve inventory and record the allocation in the same transaction when practical. For an external booking service, use its supported idempotency mechanism or stable external reference, then reconcile the destination. Local locking alone cannot make an external side effect atomic with your database.

    The receiver should verify the event's authenticity, durably record or queue valid work and acknowledge promptly. A background worker can perform slower processing. If recording fails, do not acknowledge success and lose the job.

    Give abandoned in-progress work a recovery owner; an expired lease should trigger reconciliation before another worker executes, not an unconditional retry.

    The awkward case is a send that might have succeeded

    Suppose the email provider accepted the confirmation but the network failed before you received its receipt. Your ledger says unknown. Retrying immediately may send a duplicate; marking it failed may hide a successful message.

    First use the provider's documented idempotency support, if available. Stripe's API illustrates request keys that preserve the result of a request, subject to its documented behaviour and retention. That does not mean your email provider offers the same guarantee. Stripe idempotent requests.

    Where no reliable idempotency or lookup exists, pause that action for reconciliation. Search the destination using a stable reference if supported, or route to a person with the relevant delivery evidence. This may be slower, but it preserves the distinction between known failure and uncertain success.

    Do not promise exactly-once delivery merely because the automation platform has a retry setting. Ask the implementer to explain the gap between sending the external request and saving its receipt. That is where many duplicate actions begin.

    What the second delivery should see

    Fictional action ledger
    Event received: evt_sample_A
    Order: workshop-842
    Action key: shop-1:workshop-842:reserve-seat
    State: confirmed
    Destination reservation: RSV-219
    
    Event delivered again: evt_sample_A
    Decision: acknowledge existing event; no second reservation.
    
    Different event for the same purchase: evt_sample_B
    Decision: inspect order state and existing business action.
    No second reservation merely because the event ID differs.
    
    Confirmation request times out:
    State: unknown
    Decision: reconcile provider state before retrying.

    A record like this gives a support person a useful answer when a customer says they received two messages. It also distinguishes two real purchases from one purchase delivered twice. Avoid deduplicating solely by customer email or amount: a person can legitimately buy two seats or place two equal-value orders.

    Ask for a failure demonstration before launch

    Test simultaneous deliveries, not only a replay after the first run finishes. Interrupt the worker after the external action but before it records confirmation. Deliver an older event after a newer one. Confirm that the workflow checks current state rather than blindly undoing a later update.

    Duplicate-action acceptance sheet
    Business action and stable key:
    Atomic claim mechanism:
    Destination idempotency or lookup support:
    Receipt saved on success:
    Unknown-outcome reconciliation owner:
    Replay retention period:
    
    Tests: repeated event, concurrent event, distinct event same order,
    late event, crash after action, manual rerun.
    Expected: one intended effect, or an explicit review state.

    Keep AI out of the duplicate-prevention decision. It can explain a failure to staff or draft a customer update, but the permission to create the next booking should come from recorded state and business rules. Once that boundary exists, retries become a recovery tool instead of a second sale you never meant to make.

    For a stuck in-progress action, use an explicit recovery procedure with ownership and a timeout policy. An expired worker lease should trigger reconciliation, not an assumption that the external action never happened.

    Sources and dates

    Sources checked on 20 October 2026. Worked examples are teaching examples unless explicitly attributed. Provider documentation describes product behaviour; the operating recommendations are DLVX editorial judgement.

    • Stripe webhooksDuplicate delivery, event ordering and prompt acknowledgment. Checked 2026-10-20.
    • Stripe idempotent requestsProvider request idempotency has documented semantics; not a guarantee for other services. Checked 2026-10-20.

    Have a workflow in mind? Talk through the job with DLVX.