Solana Pay Integration Guide (2026): QR Checkout, Real Pitfalls, and the Finality Shortcut

·8 min read

Solana Pay is an open, fee-free QR payment spec — and integrating it well is harder than the hello-world suggests. A guide from a team that ships it in production: the URI format, the three pitfalls that actually bite, what comes after the QR code, and the one-API-call shortcut.

Contents
  1. 1What Solana Pay actually is
  2. 2Building the URI (with real code)
  3. 3The three pitfalls that actually bite
  4. 4The part nobody's tutorial covers: after the scan
  5. 5The shortcut: one API call, QR included
  6. 6FAQ

The short answer: Solana Pay is an open specification (Apache-2.0, no fees, no middleman) that encodes a complete payment — recipient, amount, token, label — into a URI any Solana wallet can scan as a QR code. Integrating the URI takes an afternoon; integrating payments — detection, amount matching, reconciliation, settlement — is the real project. This guide covers both honestly: how to build the QR side yourself, the three pitfalls we hit shipping it in production at Finality, and the shortcut where one API call returns a checkout with Solana Pay QR, exchange-transfer support, and instant on-chain fee-splitting already done.

What Solana Pay actually is

Solana Pay defines two request types, and knowing the difference saves you a week:

For merchant checkout, Transfer Request is the right tool: no server round-trip, works printed on paper, works on a phone screen, works in an email.

Building the URI (with real code)

The format, from the spec:

code
solana:<recipient>?amount=<decimal>&spl-token=<mint>&label=<merchant>&message=<order>

A production-shaped builder:

ts
export function buildSolanaPayUri(opts: {
  recipient: string;      // the payment address
  amount: string;         // human-readable decimal: "49.00" — NOT atomic units
  splToken?: string | null; // SPL mint; omit for native SOL
  label?: string;         // shown in the wallet's confirm screen
  message?: string;       // e.g. your order reference
}): string {
  const p = new URLSearchParams();
  p.set("amount", opts.amount);
  if (opts.splToken) p.set("spl-token", opts.splToken);
  if (opts.label) p.set("label", opts.label);
  if (opts.message) p.set("message", opts.message);
  return `solana:${opts.recipient}?${p.toString()}`;
}

Render that as a QR code and every major Solana wallet — Phantom included — scans it natively. Omit spl-token and the wallet sends native SOL; include a mint (say, USDC's) and the wallet handles the token transfer, deriving the associated token account itself.

The three pitfalls that actually bite

These are not theoretical — each one is a bug class we've either hit or watched others hit in production:

1. amount is a decimal, not atomic units. The spec is explicit: use uiAmountString, not the raw token amount. USDC has 6 decimals — encode 49000000 where you meant 49.00 and the wallet will politely ask your customer to approve a $49 million payment. What makes this genuinely dangerous: EIP-681, the equivalent EVM standard, is the exact opposite — it takes atomic units (wei). If your checkout supports both ecosystems, this is the single easiest thing to get backwards.

2. A bare-address QR code is an architectural contradiction. The lazy integration encodes just the address and lets the customer type the amount. But if your order model requires an exact amount — and any serious payment system does — a hand-typed amount that's off by a cent leaves the order stuck pending forever while the money sits at the address. A payment model with zero tolerance on amounts, paired with a QR code that doesn't carry the amount, is a system designed to generate support tickets. Always encode the full URI.

3. label and message are not decoration. They're what the customer sees on the wallet's confirmation screen. An unlabeled payment request from an unknown address looks like a phishing attempt; a labeled one looks like a checkout. Put your store name in label and the order reference in message — it also makes the customer's own history legible.

The part nobody's tutorial covers: after the scan

Here's the honest scope creep. The URI was the easy 10%. A customer has now sent 49 USDC to your address — and your work begins:

None of this is impossible. All of it is undifferentiated infrastructure that has nothing to do with your product.

The shortcut: one API call, QR included

This is exactly the layer Finality sells, so here's the pitch stated plainly. One call:

bash
curl -X POST https://finality.money/v1/intents \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "chain": "solana",
    "token": "USDC",
    "amount": "49.00",
    "merchant_order_id": "ORD-3141"
  }'

returns a dedicated payment address and a hosted checkout that already renders the correct Solana Pay URI (decimal amount, right mint, your label) — plus an EIP-681 QR when the customer picks an EVM chain instead, because the same checkout covers five chains. Then the parts you didn't want to build:

And because vaults and settlement rules live on-chain, two things exist here that a DIY integration (or any other gateway) can't offer: real auto-charging subscriptions on Solana — billing terms welded into a customer-controlled vault's address, no renewal emails (architecture) — and affiliate commissions that split inside the customer's payment transaction, straight to each affiliate's wallet.

FAQ

Which wallets support Solana Pay QR codes?

Effectively all major Solana wallets, Phantom first among them. The URI scheme also works as a tap-through deep link on mobile.

What about customers who don't have a wallet at all?

With per-order dedicated addresses, an exchange withdrawal is a payment — the customer sends from their Binance/Coinbase account like any transfer. On Finality this works out of the box, QR or not.

Can I test without real money?

Yes — create a sandbox payment with a sk_test_ key and use "simulate payment" to rehearse the full detect-settle-webhook cycle, QR included.

Does one integration cover EVM chains too?

On Finality, yes — the checkout emits Solana Pay URIs on Solana and EIP-681 URIs on Ethereum, Base, and BNB Chain, with the decimal-vs-atomic difference handled for you. That last clause is worth more than it sounds; see pitfall #1.

What is Solana Pay, in one paragraph?

A URI scheme: destination, amount, token, label, message — encoded so a wallet can scan, pre-fill, and send in one motion. It's the amount-in-code rule given a formal grammar; the payment itself is an ordinary Solana transfer.

How do I generate a Solana Pay QR code?

Encode the URI, render it as QR — or skip the assembly: every Finality payment ships with its QR already encoding address, amount, and your store label, pitfalls pre-handled.

Is Solana Pay only for SOL, or does it support USDC?

Any SPL token via the spl-token parameter — USDC is the working currency in practice. Omit the parameter and you've requested native SOL; a checkout should always be explicit.

How fast do Solana Pay payments confirm?

It's a normal Solana transfer under the hood — sub-second detection, measured. The customer watches the paid-state flip while the phone is still in their hand.

What fees apply to Solana Pay transactions?

The customer pays the network fee — fractions of a cent, median priority fee zero in our measurements. The merchant side is your gateway's cut; here, the flat 1%.

How do I verify a Solana Pay payment actually settled?

Not by trusting the wallet's success screen — by watching the chain: match the transfer to the dedicated address and amount, then act on the signed payment.settled webhook. Detection is the integration's real work.

Can Solana Pay handle refunds?

The scheme requests payments; refunds are ordinary transfers you initiate — with the usual exchange-address caveat. Design your flow to collect a wallet address at refund time.

Does Solana Pay work for in-person checkout?

It's arguably the best fit — the counter guide is built on exactly this: printed or displayed QR, customer's phone as the terminal, confirmation on your dashboard in about a second.

What's the transaction-request flavor of Solana Pay?

Beyond static transfer URIs, the spec supports a server round-trip where the wallet fetches a transaction to sign — powerful for complex flows, overkill for checkout. Payments-shaped integrations rarely need it; the simple URI covers the store.

Do I need my own RPC node for Solana Pay detection?

For toy volumes, public RPCs limp along; for revenue, detection reliability is the whole product — why we run our own nodes is that argument, and using a gateway means inheriting its infrastructure instead of building yours.

Build the URI yourself for the learning, or skip to accepting: create a sandbox payment and scan your first Solana Pay QR in 15 minutes — detection, settlement, and the webhook already wired, and the money landing in a wallet only you control. Full reference in the docs and the API explorer.

Related reading