Fulfil an order
Turn a confirmed purchase into delivered access, even when events repeat or a worker crashes.
Register and test your webhook before creating the invoice or opening
checkout. For the Founder memo circle example, match webhook data.id
with Natya's order/invoice 9001. The top-level id identifies the delivery,
not the order.
Payment is not the same as completed fulfillment
| Event | What changed | What it does not prove |
|---|---|---|
order.paid | SellApp entered the paid order state and queued fulfillment. | Fulfillment or your external access grant has finished. |
order.completed | SellApp entered its completed order state after its completion workflow. | Every external delivery, community grant, notification, or receiver job has finished. |
The order tracks the purchase as a whole; its line items track delivery for each purchased product. Read the current order/invoice and relevant line items when deciding which access to grant. A browser return is not evidence of payment, and events can arrive out of order.
Commit recoverable work before returning 2xx
Think of your receiver as a reliable inbox: first save the event, then do the work. If your app restarts, that saved event tells it what still needs doing.
Verify the exact raw body with a constant-time signature comparison, then check that the payload has the expected fields and meets your event-age policy. For each accepted event:
- Begin a database transaction.
- Insert an inbox row with a unique delivery
id, the full verified payload, event/version, receipt time, andpendingprocessing state. - Save that row and, if needed, an outbox entry for sending work to a queue in the same transaction.
- Return
2xxonly after the commit succeeds. - Let a background worker claim pending inbox rows, do the work safely even if retried, and mark each row processed only after that work succeeds.
A worker that checks the saved inbox rows does not need a separate queue message. If you use a queue service, an outbox is a database record of the message that still needs sending. Save it with the inbox row, then send from that record. This closes the gap where a crash could leave a saved event with no queued work.
Do not save only a deduplication ID, acknowledge, and then enqueue the payload. A crash in that gap loses the work while making the retry look already handled.
On duplicate delivery, return 2xx only if the original payload and recoverable
work were committed. Do not enqueue a second copy or treat pending as
processed. If storage is unavailable, return a failure so SellApp can retry.
Recover after a crash
Workers need retries and a way to reclaim abandoned jobs. For example, a time-limited claim (a lease) lets another worker take over after the original one crashes. Move repeatedly failing jobs to a failed-job queue for inspection and alert your team. Keep enough payload and progress information to resume safely.
Make the action itself safe to repeat (idempotent): use a stable operation key such as
reading-room:9001:4321 for Natya's access grant, not only the delivery ID.
Separate events about the same purchase can have different delivery IDs. If
an external service succeeds but your worker crashes before marking the inbox
processed, retrying must not grant or charge twice. Use the external service's
idempotency support or check whether the action already happened there.
This pattern supports recoverable, idempotent processing; it does not promise
exactly-once execution across independent systems. Retrieve the current resource
before applying out-of-order events and do not undo completed work
because an older order.paid event arrived later.
Reconcile missed or failed work
Normal SellApp delivery makes at most two attempts. Your recovery plan should include alerts for failed delivery, manual redelivery, and periodic checks that your app agrees with SellApp's order state. Do not assume indefinite automatic retry. Test duplicates, reversed arrival order, database failure before commit, and a worker crash after an external effect.
The invoice resource exposes fulfillment, dynamic-delivery, and notification retry operations. Read current state first: these retry existing work and should not create another purchase.
For signatures, acknowledgement deadlines, test events, and redelivery controls, see Webhooks.