Accept a one-time payment

Create a payment, present checkout, and confirm the result end-to-end.

View as MarkdownInstall skills

This guide takes you from zero to a confirmed payment using VINR-hosted Checkout. It's runnable against the sandbox — swap your test keys in and follow along.

OverviewAsk

your server          VINR              customer
    │  create payment  │                   │
    │─────────────────►│                   │
    │  checkoutUrl     │                   │
    │◄─────────────────│   redirect        │
    │──────────────────────────────────────►│ pays on hosted page
    │  payment.completed (webhook)          │
    │◄─────────────────│◄──────────────────│
    │  fulfil order    │                   │

You'll create the payment on your server, send the customer to the hosted page, and fulfil on the payment.completed webhook.

Create a paymentAsk

On your backend, create a payment and read the checkoutUrl from the response.

import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

export async function POST(req: Request) {
  const { orderId } = await req.json();

  const payment = await vinr.payments.create(
    {
      amount: 4999,                 // €49.99
      currency: 'EUR',
      description: `Order ${orderId}`,
      returnUrl: `https://yoursite.com/orders/${orderId}/complete`,
      metadata: { orderId },
    },
    { idempotencyKey: `order-${orderId}` },   // safe to retry
  );

  return Response.json({ checkoutUrl: payment.checkoutUrl });
}

The idempotencyKey guarantees that a retried request (timeout, refresh) reuses the same payment instead of charging twice. See Idempotency.

Collect payment detailsAsk

Redirect the customer to the hosted page. VINR handles card entry, wallets, 3D Secure, and any local methods you've enabled.

const { checkoutUrl } = await fetch('/api/checkout', {
  method: 'POST',
  body: JSON.stringify({ orderId: '1234' }),
}).then((r) => r.json());

window.location.href = checkoutUrl;

Confirm & handle the resultAsk

When the customer returns to your returnUrl, don't trust the redirect alone — confirm the status server-side before showing success.

const payment = await vinr.payments.retrieve(paymentId);
if (payment.status === 'completed') {
  // show success — but fulfilment happens on the webhook (below)
}

Verify with a webhookAsk

Fulfil the order from the webhook so it happens exactly once, even if the customer closes the tab. See the payment lifecycle for every event.

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

  switch (event.type) {
    case 'payment.completed':
      await fulfillOrder(event.data.metadata.orderId);   // idempotent!
      break;
    case 'payment.failed':
      await notifyCustomer(event.data.metadata.orderId);
      break;
  }
  return new Response('OK', { status: 200 });
}

Test itAsk

Use these sandbox cards on the hosted page:

CardResult
4242 4242 4242 4242Success
4000 0000 0000 0002Declined
4000 0000 0000 32203D Secure challenge

Go liveAsk

Swap to live keys

Replace your sandbox VINR_SECRET_KEY with the live key from the Dashboard.

Register your production webhook

Point a webhook endpoint at your live URL and store the signing secret.

Walk the go-live checklist

Confirm error handling, idempotency, and monitoring with the go-live checklist.

Next stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page