Skip to main content

The required guarantees

Skinloop delivers webhooks at least once, and status polling can observe the same completed checkout concurrently. Never call an external fulfillment service directly from either handler. Use two independent safeguards:
  1. Store one durable fulfillment job for each checkout in your database.
  2. Send the same stable idempotency key to the fulfillment destination on every attempt.
The database constraint prevents polling and webhook workers from creating different jobs. The destination’s idempotency support prevents a retry after an ambiguous response from delivering twice.
Exactly-once external delivery cannot be guaranteed if the fulfillment destination does not support idempotency or lookup by a stable key. After an ambiguous timeout, stop automatic retries and reconcile manually rather than risk delivering twice.

Store a durable job

Adapt this PostgreSQL schema to your order system:
Use a deterministic key that remains unchanged across webhook deliveries, polling, worker restarts, and deployments:
Do not use a webhook event ID as the fulfillment key. Different events or a status poll can report the same completed checkout.

Use one enqueue operation

Both the signed webhook handler and status poller must call this operation. It records authoritative completion and creates the job in one local transaction:
Validate that the completed checkout belongs to the expected order, amount, and currency before enqueueing. The unique constraints make repeated webhook deliveries and concurrent polling harmless.

Claim work with a lease

A worker claims one job in a short database transaction, then commits before making the external request:
The lease allows another worker to recover a job if the current worker crashes before finishing. Size the lease above the normal fulfillment request timeout and renew it for longer operations. The unique lease_token prevents a stale worker from acknowledging a job after another worker has reclaimed it.

Deliver outside the transaction

Send the stored key to a destination that guarantees repeated requests with that key return the original delivery result:
Never hold a database transaction open during the network request.

Recover every crash point

For an ambiguous result without destination idempotency, mark the job for reconciliation:
An operator must then check the destination before deciding whether to mark the job succeeded or return it to queued.

Production checklist

  • Verify Skinloop signature and timestamp before recording a webhook.
  • Deduplicate webhook storage by event id.
  • Re-read or validate authoritative completed status before enqueueing.
  • Validate merchant order ID, checkout ID, amount, and currency.
  • Enforce unique checkout, order, and fulfillment keys in the database.
  • Commit the job before making any external request.
  • Use bounded timeouts and leases.
  • Require the matching lease token for success and failure updates.
  • Reuse the same fulfillment key forever for that checkout.
  • Alert on needs_reconciliation and expired processing leases.
  • Record the destination reference before marking the merchant order fulfilled.
Expected result: webhook duplicates, concurrent polling, worker restarts, and crashes do not create a second delivery. Ambiguous results are either resolved through the destination’s idempotency key or stopped for manual reconciliation.