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 fire so your system can stay in sync.
Status referenceAsk
Prop
Type
State diagramAsk
┌───────────────┐
│ pending │
└──────┬────────┘
┌──────────┼─────────────────┐
▼ ▼ ▼
requires_ processing cancelled
authentication │
│ ▼
└────► completed ──► refunded
│
failedTerminal vs. non-terminal statesAsk
- 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.completedis the only state in which you should fulfil.refundedis reachable only fromcompleted.
Fulfil on the payment.completed webhook, not on the synchronous API response. A payment can still require authentication after creation.
Webhook events per transitionAsk
| Transition | Event |
|---|---|
→ requires_authentication | payment.requires_authentication |
→ processing | payment.processing |
→ completed | payment.completed |
→ failed | payment.failed |
→ cancelled | payment.cancelled |
→ refunded | payment.refunded |
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 & retriesAsk
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 so a retried request never double-charges.
Next stepsAsk
Webhooks
Receive and verify events reliably.
Refunds
Return funds and track refund status.
Authorize & capture
Separate reserving funds from capturing them.
Last updated on