Accept stablecoins

Accept stablecoins — a runnable, end-to-end guide verified against the VINR sandbox.

View as MarkdownInstall skills

Accept USDC and EURC payments and settle the proceeds straight to your fiat balance — no wallet, custody, or blockchain code on your side. VINR handles address generation, on-chain monitoring, and conversion; you work with the same pay_ objects and webhooks you already use for cards.

OverviewAsk

A stablecoin payment is a regular VINR payment with a crypto payment method. You create it server-side, send the customer to a hosted page that shows a deposit address and QR code, and the payment confirms once the chain reaches finality.

your server          VINR                 chain / customer
    │  create payment   │                       │
    │──────────────────►│                       │
    │  checkoutUrl      │   deposit address      │
    │◄──────────────────│──────────────────────►│ sends USDC
    │                   │   payment.processing   │
    │◄──────────────────│◄──────────────────────│ tx seen
    │  payment.completed │   (after finality)    │
    │◄──────────────────│                       │
    │  fulfil order     │                       │

The amount is quoted in your fiat currency (minor units, EUR by default). VINR locks an exchange rate at creation time and shows the customer the equivalent token amount.

Supported assets and chainsAsk

AssetNetworksNotes
USDCEthereum, Base, Polygon, SolanaMost liquid; lowest fees on Base and Solana
EURCEthereum, BaseNative EUR settlement, no FX spread
USDTEthereum, PolygonSettlement to EUR incurs an FX conversion

Enable assets and networks per account in Dashboard → Payment methods → Stablecoins. Networks you have not enabled are rejected at payment creation with a 400 unsupported_network.

Create a stablecoin paymentAsk

Pass paymentMethod: 'stablecoin' and the assets you accept. The customer picks the exact token and network on the hosted page.

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 — quoted, then converted to tokens
      currency: 'EUR',
      paymentMethod: 'stablecoin',
      stablecoin: {
        assets: ['USDC', 'EURC'],          // what you'll accept
        networks: ['base', 'ethereum'],    // optional allow-list
      },
      description: `Order ${orderId}`,
      returnUrl: `https://yoursite.com/orders/${orderId}/complete`,
      metadata: { orderId },
    },
    { idempotencyKey: `order-${orderId}` },
  );

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

Redirect the customer to payment.checkoutUrl. The hosted page displays a unique deposit address, a QR code, the locked token amount, and a countdown to the rate-quote expiry.

The locked rate expires (default 15 minutes). If the customer pays after expiry or sends the wrong amount, the payment moves to requires_action and VINR re-quotes or initiates a refund of the underpayment. Always fulfil on the webhook, never on the redirect.

Confirmation and finalityAsk

On-chain payments are not instant. A stablecoin payment passes through extra states before completed:

Prop

Type

Required confirmations vary by network — VINR waits for finality before emitting payment.completed, so you never fulfil on a reorg-able transaction.

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.processing':
      await markAwaitingConfirmation(event.data.metadata.orderId);
      break;
    case 'payment.completed':
      await fulfillOrder(event.data.metadata.orderId);   // idempotent!
      break;
    case 'payment.requires_action':
      await notifyCustomer(event.data.metadata.orderId);  // under/late payment
      break;
  }
  return new Response('OK', { status: 200 });
}

The completed event includes a stablecoin block with the settled token, network, txHash, and confirmation count for your records and reconciliation.

Settlement to fiatAsk

By default, VINR auto-converts received tokens to your account currency and adds the proceeds to your fiat balance, which pays out via the normal payout schedule. EURC into a EUR account settles 1:1 with no FX spread; USD-denominated tokens incur a conversion at the quoted rate.

To hold the asset instead of converting, set stablecoin.settlement: 'crypto' at creation — proceeds accrue to a per-asset crypto balance you can withdraw to an external address. See settlements for the conversion ledger and fee breakdown.

Test itAsk

The sandbox simulates the chain so no real funds or wallet are needed. Open the hosted page, pick an asset, then trigger an outcome:

Sandbox actionResult
Simulate payment buttonFunds the address with the exact amount → completed
Simulate underpaymentSends less than quoted → requires_action
Let quote expireWait past the countdown → requires_action, then auto re-quote

You can also drive it from your server against https://sandbox.api.vinr.com:

curl https://sandbox.api.vinr.com/v1/test/stablecoin/fund \
  -H "X-Api-Key: $VINR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"payment": "pay_3Nf8...", "asset": "USDC", "network": "base"}'

Go liveAsk

Enable mainnet assets

Switch the stablecoin payment method to live in the Dashboard and confirm the networks you support in production.

Choose a settlement policy

Decide between fiat (auto-convert) and crypto (hold) settlement, and review the FX spread on the pricing page.

Walk the go-live checklist

Confirm webhook handling for processing and requires_action, then run the go-live checklist.

Next stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page