Go omnichannel

Link your online and in-store channels so shoppers can save cards online and reuse them in person, and earn loyalty on every purchase.

View as MarkdownInstall skills

This guide walks you through connecting your online checkout to your in-store terminal so shoppers can save a card on your website, pay faster at the counter, and see their loyalty balance grow across both channels. By the end you'll have a single shopperReference tying every purchase together, a saved card token reusable at the terminal, and a webhook handler that credits loyalty points regardless of where the payment happens.

OverviewAsk

online checkout (save card)
    │  payments.create({ shopperReference, setupFutureUsage: 'recurring' })
    │─────────────────────────────────────────► VINR
    │  pm_… token                              │
    │◄──────────────────────────────────────────│
    │  store pm_ against shopperReference      │


in-store terminal (reuse token)
    │  terminal.payments.create({ shopperReference, paymentMethod: 'pm_…' })
    │─────────────────────────────────────────► VINR ──► terminal
    │  terminal_payment.completed (webhook)    │
    │◄──────────────────────────────────────────│


loyalty credited across channels
    │  engagement.loyalty.credit({ shopperReference, orderId, amount })
    │─────────────────────────────────────────► VINR

Step 1 — Identify shoppers with a shopperReferenceAsk

A shopperReference is the stable identifier you assign to each customer. Add it to every payments.create and terminal.payments.create call so VINR can link the shopper's history across channels.

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

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

const onlinePayment = await vinr.payments.create(
  {
    amount: 7500,
    currency: 'EUR',
    description: 'Order ord_9921',
    returnUrl: 'https://yoursite.com/orders/ord_9921/complete',
    shopperReference: 'ada@example.com',
    shopperEmail: 'ada@example.com',
    metadata: { orderId: 'ord_9921' },
  },
  { idempotencyKey: 'order-ord_9921' },
);

const terminalPayment = await vinr.terminal.payments.create(
  {
    terminalId: 'term_store01_lane2',
    amount: 3200,
    currency: 'EUR',
    shopperReference: 'ada@example.com',
    shopperEmail: 'ada@example.com',
    metadata: { orderId: 'ord_9944' },
  },
  { idempotencyKey: 'order-ord_9944' },
);

Prop

Type

shopperReference must be stable. Never change it for a returning customer — doing so breaks the link to their saved cards and loyalty account across every channel.

Step 2 — Save a card during online checkout and reuse it in storeAsk

Set setupFutureUsage: 'recurring' on the online payment to instruct VINR to store the card and run the authentication needed for future merchant-initiated charges.

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

  const payment = await vinr.payments.create(
    {
      amount: 7500,
      currency: 'EUR',
      description: `Order ${orderId}`,
      returnUrl: `https://yoursite.com/orders/${orderId}/complete`,
      shopperReference,
      shopperEmail: `${shopperReference}`,
      setupFutureUsage: 'recurring',
      metadata: { orderId, customerId },
    },
    { idempotencyKey: `order-${orderId}` },
  );

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

When the payment completes, VINR emits payment.completed with a paymentMethod field containing the saved pm_… token. Persist it against the shopperReference.

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' && event.data.paymentMethod) {
    await db.saveToken({
      shopperReference: event.data.shopperReference,
      paymentMethodId: event.data.paymentMethod.id,
      brand: event.data.paymentMethod.card.brand,
      last4: event.data.paymentMethod.card.last4,
    });
  }
  return new Response('OK', { status: 200 });
}

At the terminal, look up the saved token for the shopper and pass it directly. VINR presents a tap-to-confirm prompt on the terminal; the customer does not re-enter their card details.

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

  const { paymentMethodId } = await db.getToken(shopperReference);

  const payment = await vinr.terminal.payments.create(
    {
      terminalId: 'term_store01_lane2',
      amount: 3200,
      currency: 'EUR',
      shopperReference,
      paymentMethod: paymentMethodId,
      metadata: { orderId },
    },
    { idempotencyKey: `order-${orderId}` },
  );

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

The customer taps to confirm at the terminal but does not re-enter card details. VINR uses the stored recurring mandate, so no additional 3D Secure challenge is required.

Step 3 — Credit loyalty on every channelAsk

Both payment.completed (online) and terminal_payment.completed (in-store) carry shopperReference. Handle them in the same webhook so loyalty points accumulate regardless of channel.

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

  const isPaymentEvent =
    event.type === 'payment.completed' ||
    event.type === 'terminal_payment.completed';

  if (!isPaymentEvent) {
    return new Response('OK', { status: 200 });
  }

  const { shopperReference, id: paymentId, amount, metadata } = event.data;

  await vinr.engagement.loyalty.credit(
    {
      shopperReference,
      orderId: metadata.orderId,
      amount,
      source: { payment: paymentId },
    },
    { idempotencyKey: `loyalty-${metadata.orderId}` },
  );

  return new Response('OK', { status: 200 });
}

Points are reflected in the shopper's loyalty account immediately. Their balance appears on their next digital receipt and in any loyalty UI you build using the cross-channel loyalty API.

What you've builtAsk

Unified shopper identity

Every payment — online or in-store — now carries a shopperReference that VINR uses to link saved cards, purchase history, and loyalty across channels.

Frictionless in-store checkout

A shopper who saves their card on your website can pay at any terminal with a single tap. No card re-entry, no additional authentication prompts.

Channel-agnostic loyalty

Both payment.completed and terminal_payment.completed feed the same loyalty credit handler, so points accumulate on every purchase. The shopper sees a unified balance on their next digital receipt.

Your shoppers can now save a card on your website, pay faster in store, and see their loyalty points balance grow across both channels. Explore the full unified commerce feature set at Omnichannel payments.

Next stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page