Payment lifecycle

Every payment status and the transitions between them.

View as MarkdownInstall skills

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

                 failed

Terminal 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. 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 transitionAsk

TransitionEvent
requires_authenticationpayment.requires_authentication
processingpayment.processing
completedpayment.completed
failedpayment.failed
cancelledpayment.cancelled
refundedpayment.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

Was this page helpful?
Edit on GitHub

Last updated on

On this page