# Payment lifecycle

> Every payment status and the transitions between them.

Each payment has exactly one `status` at any moment. This page documents every status, what moves a payment between them, and which [webhook events](/docs/integration/webhooks) fire so your system can stay in sync.

## Status reference

| Field                     | Type     | Description                                             | Default |
| ------------------------- | -------- | ------------------------------------------------------- | ------- |
| `pending`                 | `status` | Created, awaiting customer action or processing.        | `—`     |
| `requires_authentication` | `status` | Customer must complete 3D Secure / SCA.                 | `—`     |
| `processing`              | `status` | Authorized and being captured or confirmed on the rail. | `—`     |
| `completed`               | `status` | Funds captured and credited toward your balance.        | `—`     |
| `failed`                  | `status` | Declined or could not be processed.                     | `—`     |
| `cancelled`               | `status` | Voided before capture.                                  | `—`     |
| `refunded`                | `status` | Fully returned to the customer after completion.        | `—`     |

## State diagram

```
            ┌───────────────┐
            │    pending    │
            └──────┬────────┘
        ┌──────────┼─────────────────┐
        ▼          ▼                 ▼
requires_      processing         cancelled
authentication     │
        │          ▼
        └────► completed ──► refunded
                   │
                 failed
```

## Terminal vs. non-terminal states

- **Non-terminal** (`pending`, `requires_authentication`, `processing`) — the payment may still change. Never fulfil an order in these states.
- **Terminal** (`completed`, `failed`, `cancelled`, `refunded`) — the outcome is settled. `completed` is the only state in which you should fulfil. `refunded` is reachable only from `completed`.

> Fulfil on the `payment.completed` **webhook**, not on the synchronous API response. A payment can still require authentication after creation.

## Webhook events per transition

| Transition                  | Event                             |
| --------------------------- | --------------------------------- |
| → `requires_authentication` | `payment.requires_authentication` |
| → `processing`              | `payment.processing`              |
| → `completed`               | `payment.completed`               |
| → `failed`                  | `payment.failed`                  |
| → `cancelled`               | `payment.cancelled`               |
| → `refunded`                | `payment.refunded`                |

```typescript
export async function POST(req: Request) {
  const event = vinr.webhooks.verify(
    await req.text(),
    req.headers.get('x-vinr-signature'),
  );

  if (event.type === 'payment.completed') {
    await fulfillOrder(event.data.metadata.orderId);
  }
  return new Response('OK', { status: 200 });
}
```

## Idempotency & retries

Webhook delivery is at-least-once, so the same event may arrive more than once. Make handlers **idempotent** — key on `event.id` and ignore duplicates. When creating payments, send an [idempotency key](/docs/api-reference/idempotency) so a retried request never double-charges.

## Next steps

[Webhooks](/docs/integration/webhooks) — Receive and verify events reliably.

[Refunds](/docs/payments/refunds) — Return funds and track refund status.

[Authorize & capture](/docs/payments/authorize-and-capture) — Separate reserving funds from capturing them.
