# Payment lifecycle

Orders, authorizations, captures and refunds keep PayPal's states, fee breakdowns and errors; an order is approved by a seed, and a card order can complete at creation.

The payment lifecycle is how an order becomes money and how money comes back: an order is approved, captured or authorized, and each capture can be refunded. The simulator keeps the amounts, fee breakdowns, ids and refusals of PayPal's sandbox along the way, so a checkout integration can exercise every step without a browser. Amounts are decimal strings in the currency's own unit, as on PayPal, so `"25.00"` is $25.00. Seed an approved order, capture it and refund part of the capture:

```bash
curl -s -o /dev/null -w '%{http_code}\n' -X POST "$RYSTIC_URL/_rystic/seed" -d '{"state_order": {"ORDAPPROVED1": {"id": "ORDAPPROVED1", "intent": "CAPTURE", "status": "APPROVED", "create_time": "2026-01-01T00:00:00Z", "update_time": "2026-01-01T00:00:00Z", "purchase_units": [{"reference_id": "default", "amount": {"currency_code": "USD", "value": "25.00"}, "payee": {"merchant_id": "MERCHANT1"}}]}}}'
# -> 204
curl -s -X POST "$RYSTIC_URL/v2/checkout/orders/ORDAPPROVED1/capture" -H 'Authorization: Bearer offline-test' -H 'Content-Type: application/json' -H 'PayPal-Request-Id: cap-1' -d '{}' | jq -c '{status, capture: .purchase_units[0].payments.captures[0].id}'
# -> {"status":"COMPLETED","capture":"C0000000000000001"}
curl -s "$RYSTIC_URL/v2/payments/captures/C0000000000000001" -H 'Authorization: Bearer offline-test' | jq -c '{status, fee: .seller_receivable_breakdown.paypal_fee.value}'
# -> {"status":"COMPLETED","fee":"1.36"}
curl -s "$RYSTIC_URL/v2/payments/captures/C0000000000000001/refund" -H 'Authorization: Bearer offline-test' -H 'Content-Type: application/json' -H 'PayPal-Request-Id: refund-1' -d '{"amount":{"currency_code":"USD","value":"10.00"}}' | jq -c '{id, status, fee: .seller_payable_breakdown.paypal_fee.value}'
# -> {"id":"RF000000000000001","status":"COMPLETED","fee":"0.00"}
curl -s "$RYSTIC_URL/v2/payments/captures/C0000000000000001/refund" -H 'Authorization: Bearer offline-test' -H 'Content-Type: application/json' -H 'PayPal-Request-Id: refund-2' -d '{"amount":{"currency_code":"USD","value":"20.00"}}' | jq -c '{name, issue: .details[0].issue}'
# -> {"name":"UNPROCESSABLE_ENTITY","issue":"REFUND_AMOUNT_EXCEEDED"}
curl -s "$RYSTIC_URL/_rystic/egress" | jq -c '[.events[].type]'
# -> ["PAYMENT.CAPTURE.REFUNDED"]
```

### Orders and approval

A wallet order starts `CREATED`. Buyer approval is a step on paypal.com, so an order reaches `APPROVED` only through a seed or the [Business UI](https://www.rystic.ai/docs/simulators/paypal/features/business-ui.md)'s **Approve**, with either intent. `design_studio` holds no approved order.

A card order can complete when you create it. A card order is one whose `payment_source` holds a card and whose creating request carries a `PayPal-Request-Id`. What comes back depends on its intent:

| `intent` | The order comes back |
|---|---|
| `CAPTURE` | `COMPLETED`, with its capture |
| `AUTHORIZE` | `COMPLETED`, holding an authorization |

So capturing and voiding an authorization work without a seed. Read an order at `GET /v2/checkout/orders/{id}` and change its supported fields with `PATCH`. Capturing an order a second time returns 422 `ORDER_ALREADY_CAPTURED`.

### Authorizations

An authorization holds an amount for 29 days from its creation. Capture some or all of it with `POST /v2/payments/authorizations/{authorization_id}/capture`:

| You capture | The simulator |
|---|---|
| part of what is left | leaves the authorization `PARTIALLY_CAPTURED` |
| more than what is left | returns 422 `MAX_CAPTURE_AMOUNT_EXCEEDED` |

Void it with `POST .../void`, which returns an empty 204. The example runs against a card `AUTHORIZE` order, and the log still holds the refund from the first block:

```bash
curl -s "$RYSTIC_URL/v2/checkout/orders" -H 'Authorization: Bearer offline-test' -H 'Content-Type: application/json' -H 'PayPal-Request-Id: card-1' -d '{"intent":"AUTHORIZE","purchase_units":[{"amount":{"currency_code":"USD","value":"40.00"}}],"payment_source":{"card":{"number":"4111111111111111","expiry":"2030-12"}}}' | jq -c '{id, status, authorization: .purchase_units[0].payments.authorizations[0].id}'
# -> {"id":"ORD00000000000001","status":"COMPLETED","authorization":"AUT00000000000001"}
curl -s "$RYSTIC_URL/v2/payments/authorizations/AUT00000000000001/capture" -H 'Authorization: Bearer offline-test' -H 'Content-Type: application/json' -H 'PayPal-Request-Id: capture-1' -d '{"amount":{"currency_code":"USD","value":"15.00"},"final_capture":false}' | jq -c '{status}'
# -> {"status":"COMPLETED"}
curl -s "$RYSTIC_URL/v2/payments/authorizations/AUT00000000000001" -H 'Authorization: Bearer offline-test' | jq -c '{id, status}'
# -> {"id":"AUT00000000000001","status":"PARTIALLY_CAPTURED"}
curl -s -o /dev/null -w '%{http_code}\n' -X POST "$RYSTIC_URL/v2/payments/authorizations/AUT00000000000001/void" -H 'Authorization: Bearer offline-test'
# -> 204
curl -s "$RYSTIC_URL/_rystic/egress" | jq -c '[.events[].type]'
# -> ["PAYMENT.CAPTURE.REFUNDED","PAYMENT.CAPTURE.COMPLETED","authorization_voided"]
```

### Fees and refunds

A USD capture carries a fixed synthetic fee of 3.49% plus $0.49, rounded to cents. The same fee applies whether the capture comes from a card order, an order capture, an authorization capture or a scenario. The capture's `seller_receivable_breakdown` holds the gross, the fee and the net, as the first block shows.

**Differs from PayPal:** the fee is the simulator's own fixed pricing, not the sandbox's, and actual merchant fees vary.

**Differs from PayPal:** platform-fee return is not modeled.

**Differs from PayPal:** non-USD pricing is not modeled.

An ordinary USD refund keeps the capture's fee: its `paypal_fee` is zero and its net equals the refunded gross. A refund above what the capture has left returns 422 `REFUND_AMOUNT_EXCEEDED`. A retry with the same `PayPal-Request-Id` returns the stored refund.

### Validation

Creating an order or capturing a payment with an invalid currency, too many decimal places, a negative amount or a malformed body is refused without moving a balance. Retrying a capture or refund keeps the stored payment and the capacity it left.

### What the lifecycle does not do

A `PENDING` capture or refund stays `PENDING`: nothing settles on its own.

Reauthorizing (`POST .../reauthorize`) is allowed once, from day 4 to day 29 after the original authorization. Before day 4 it returns 422 `REAUTHORIZATION_TOO_SOON`. Authorization capacity counts every capture across the reauthorization chain, refunded ones included.

Three parts of reauthorizing are unconfirmed, meaning not yet checked against PayPal's sandbox, and tests check only that balances are conserved:

- a successful reauthorization from day 4 on;
- the replacement authorization's amount;
- the exact error for a superseded authorization.

---

Source: https://www.rystic.ai/docs/simulators/paypal/features/payment-lifecycle · Markdown: https://www.rystic.ai/docs/simulators/paypal/features/payment-lifecycle.md
