Advanced flow
Build your own payment UI and drive the full payment lifecycle directly against the VINR API — no hosted UI required.
The advanced flow gives you complete control over both the payment UI and the payment lifecycle. You collect card data in your own form, tokenize it client-side, and then orchestrate every subsequent step — confirmation, 3DS authentication, and capture — from your server. Nothing is delegated to a hosted page or a pre-built component.
This path is appropriate when Checkout and Elements cannot satisfy your UX requirements, when you need to compose payments into a larger custom flow, or when your platform must own the entire rendering layer. In exchange for that control you accept a significantly higher PCI compliance burden: because your frontend handles raw card data, you must be certified under SAQ-D (or an equivalent full ROC assessment).
For a side-by-side comparison of all integration paths, see Integration models.
PrerequisitesAsk
Before you start, make sure you have the following in place.
- SAQ-D PCI compliance — your organisation has completed, or is actively working through, an SAQ-D self-assessment questionnaire (or equivalent). Raw card numbers must never touch your servers; the tokenization step below is mandatory and must run exclusively in the browser.
- VINR SDK or direct HTTPS access — install the TypeScript SDK (
npm install @vinr/sdk) or issue signed requests directly tohttps://api.vinr.com/v1. All examples below use the SDK. - Test keys from the Dashboard — navigate to Dashboard → Developers → API keys and copy your
sk_test_secret key andpk_test_publishable key. Use the publishable key in browser code; keep the secret key server-side only.
npm install @vinr/sdkStep 1: Tokenize card dataAsk
Card tokenization converts raw card data into a reusable payment method token (pm_...) that you can pass safely to your server. This call must originate from your frontend — never from a server that logs requests, so that raw card values never appear in any log or network trace.
import { VinrClient } from '@vinr/sdk/browser';
const vinr = new VinrClient({ publishableKey: process.env.NEXT_PUBLIC_VINR_PUBLISHABLE_KEY });
const paymentMethod = await vinr.paymentMethods.create({
type: 'card',
card: {
number: cardNumberInput.value,
expMonth: parseInt(expMonthInput.value, 10),
expYear: parseInt(expYearInput.value, 10),
cvc: cvcInput.value,
},
billingDetails: {
name: nameInput.value,
},
});
// paymentMethod.id → "pm_01j9xkz8..."
// Send paymentMethod.id to your server; never send raw card values.const response = await fetch('https://api.vinr.com/v1/payment-methods', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${VINR_PUBLISHABLE_KEY}`,
},
body: JSON.stringify({
type: 'card',
card: {
number: cardNumberInput.value,
expMonth: parseInt(expMonthInput.value, 10),
expYear: parseInt(expYearInput.value, 10),
cvc: cvcInput.value,
},
}),
});
const { id: paymentMethodId } = await response.json();Never log or store raw card data. Zero raw card values — number, cvc, expMonth, expYear — should appear in console output, application logs, analytics events, or any persistent store. Once you have a pm_... token, discard the raw values immediately.
Step 2: Create and confirm the paymentAsk
On your server, create a payment and confirm it in a single call by passing confirm: true alongside the payment method token. The server-side SDK uses your secret key, which must never be exposed to the browser.
import { Vinr } from '@vinr/sdk';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });
const payment = await vinr.payments.create({
amount: 4900,
currency: 'EUR',
paymentMethod: paymentMethodId,
confirm: true,
returnUrl: 'https://yoursite.com/orders/return',
metadata: { orderId: 'ord_88821' },
});The status field on the returned payment object tells you what to do next.
if (payment.status === 'completed') {
await fulfillOrder(payment.metadata.orderId);
}The payment was authorized and captured (or authorized if you set captureMethod: 'manual'). No further action is needed from the customer.
if (payment.status === 'requires_action') {
const redirectUrl = payment.authentication.redirectUrl;
return res.json({ requiresAction: true, redirectUrl });
}The issuer requires 3DS authentication. See Step 3 below.
if (payment.status === 'failed') {
console.error(payment.lastPaymentError?.code, payment.lastPaymentError?.message);
return res.status(402).json({ error: payment.lastPaymentError });
}The payment was declined. Surface the decline reason to the customer and invite them to try a different card or contact their bank.
Step 3: Handle requires_action (3DS)Ask
When status is requires_action, the issuer has triggered a Strong Customer Authentication (3DS) challenge. You must redirect the customer — or invoke native 3DS in your mobile app — and then verify the outcome before fulfilling the order.
Redirect the customer to the authentication URL
Return the redirect URL to your frontend and navigate the customer there.
if (payment.status === 'requires_action') {
res.json({
requiresAction: true,
redirectUrl: payment.authentication.redirectUrl,
});
}if (data.requiresAction) {
window.location.href = data.redirectUrl;
}Handle the return URL
After the customer completes (or abandons) the challenge, the issuer redirects back to your returnUrl with the payment ID in the query string. Retrieve the updated payment on your server to check the final status.
const paymentId = new URL(req.url).searchParams.get('payment_id');
const payment = await vinr.payments.retrieve(paymentId);
if (payment.status === 'completed') {
await fulfillOrder(payment.metadata.orderId);
} else if (payment.authentication.status === 'failed') {
return res.status(402).json({ error: 'Authentication failed' });
}Alternatively, confirm via webhook
Instead of polling the return URL, listen for payment.completed or payment.failed on your webhook endpoint. Webhook delivery is more reliable for server-side fulfilment because it is not dependent on the customer's browser completing the redirect.
Step 4: Capture (if manual)Ask
If you created the payment with captureMethod: 'manual', the payment moves to authorized after confirmation and 3DS (if required). Funds are reserved on the card but not yet settled. Capture once you are ready to fulfil — typically after shipping physical goods or confirming a reservation.
const captured = await vinr.payments.capture(payment.id);
// captured.status → "completed"
// captured.amountCaptured → 4900You can capture a smaller amount than the original authorization. Unused funds are released automatically to the cardholder.
const captured = await vinr.payments.capture(payment.id, {
amount: 3500,
});
// captured.amountCaptured → 3500 (€35.00)
// remaining €14.00 hold releasedAuthorizations expire after 7 days by default. If you do not capture within that window the payment moves to expired and the cardholder's hold is released; you cannot capture an expired authorization.
For the full authorize-and-capture reference, including voids, see Authorize & capture.
Step 5: Verify via webhookAsk
Webhook delivery is the authoritative signal for fulfilment. Do not fulfil an order based solely on the API response from Step 2 — always wait for the signed payment.completed event before releasing goods or services.
import { Vinr } from '@vinr/sdk';
import { headers } from 'next/headers';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get('x-vinr-signature')!;
const timestamp = req.headers.get('x-vinr-timestamp')!;
const event = vinr.webhooks.constructEvent(body, signature, timestamp);
if (event.type === 'payment.completed') {
const payment = event.data.object;
await fulfillOrder(payment.metadata.orderId);
}
return new Response('OK');
}Pass the raw request body string to constructEvent — not the parsed JSON. Re-serializing changes byte order and whitespace and will break the HMAC signature check.
For endpoint registration, event types, and the retry schedule, see Webhooks.
Payment object referenceAsk
Key fields on the payment object that drive advanced-flow logic.
Prop
Type
Next stepsAsk
Integration models
Compare Checkout, Elements, and the advanced API flow side by side.
Authorize & capture
Reserve funds and capture after fulfilment using captureMethod: manual.
Webhooks
Register endpoints, verify signatures, and handle the retry schedule.
Last updated on