---
title: "Let provider events move the order · Agent Wranglers"
url: https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/events-drive-fulfilment
updated: 2026-10-20
licence: "All rights reserved"
---

# Let provider events move the order

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Payment events and reconciliation](https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation)
- Rule: Fulfil, grant access or mark a refund only when a verified provider event or API read says the money moved. Map each event type to one order transition.
- How: The rule [confirm payments on the server](https://agentwranglers.se/playbook/adversarial-testing/abuse-and-limits/confirm-payment-server-side) explains why the success page proves nothing. Verify each event as in [signed webhooks](https://agentwranglers.se/playbook/api-design/webhooks-and-events/signed-webhooks). Handle duplicates as in [idempotent receivers](https://agentwranglers.se/playbook/api-design/webhooks-and-events/idempotent-receivers), and expect any order, as in [event order](https://agentwranglers.se/playbook/api-design/webhooks-and-events/event-order). Keep one table from event type to transition. Before acting, read the payment from the provider's API and compare amount, currency and order ID with the order. Answer 2xx within 5 seconds and queue the work. Log event types you do not handle, and ignore them.
- Check: Send a signed test event for a payment whose amount differs from the order. The handler refuses to mark the order paid. Every event type enabled on the webhook endpoint appears in the mapping table or in an explicit ignore list.

Example:

```js
const EVENT_TRANSITIONS = {
  'payment_intent.processing': 'processing',
  'payment_intent.succeeded': 'paid',
  'payment_intent.payment_failed': 'payment_failed',
}

async function onPaymentEvent(event) {
  const to = EVENT_TRANSITIONS[event.type]
  if (!to) return logIgnoredEvent(event.type)
  const payment = await stripe.paymentIntents.retrieve(event.data.object.id)
  const order = await orders.get(payment.metadata.orderId)
  if (payment.amount !== order.totalMinor || payment.currency !== order.currency.toLowerCase()) {
    throw new Error(`Payment ${payment.id} does not match order ${order.id}`)
  }
  await moveOrder(db, order.id, order.state, to, event.id)
}
```

```python
EVENT_TRANSITIONS = {
    "payment_intent.processing": "processing",
    "payment_intent.succeeded": "paid",
    "payment_intent.payment_failed": "payment_failed",
}

def on_payment_event(event) -> None:
    to_state = EVENT_TRANSITIONS.get(event["type"])
    if to_state is None:
        return log_ignored_event(event["type"])
    payment = stripe.PaymentIntent.retrieve(event["data"]["object"]["id"])
    order = orders.get(payment.metadata["order_id"])
    if payment.amount != order.total_minor or payment.currency != order.currency.lower():
        raise PaymentMismatch(payment.id, order.id)
    move_order(db, order.id, order.state, to_state, event["id"])
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers payments playbook: https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/events-drive-fulfilment`.

- Next: [Wait for payment methods that settle later](https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/delayed-payment-methods)
- [Payment events and reconciliation](https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation)
