Solana Pay Integration Guide (2026): QR Checkout, Real Pitfalls, and the Finality Shortcut
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.
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:
- Transfer Request —
solana:<recipient>?amount=...&spl-token=...— is non-interactive: the QR code contains the entire payment. The wallet needs no callback to your server; the customer scans, confirms, and the transfer happens. This is what checkout needs. - Transaction Request —
solana:https://your-server/...— makes the wallet fetch a server-built transaction. Powerful for complex flows, unnecessary for "pay me 49 USDC".
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:
solana:<recipient>?amount=<decimal>&spl-token=<mint>&label=<merchant>&message=<order>A production-shaped builder:
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:
- Detection. Something must watch the chain and notice the transfer — ideally the moment it enters a block, because your customer is staring at a spinner. That means running or renting reliable node infrastructure.
- Amount matching. Is 48.99 acceptable? What about a payment in two installments? What arrives after your quote expired?
- Address architecture. One shared address for all orders means matching payments to orders by amount heuristics — fragile and ugly. Per-order dedicated addresses make reconciliation 1:1, but now you're managing address derivation at scale.
- Settlement and fees. If anything needs splitting — platform fees, partner commissions — you're writing on-chain settlement logic.
- Notifying your backend. Signed webhooks, retries, idempotency — the boring plumbing that decides whether you double-ship an order.
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:
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:
- Detection by our own full nodes — the payment flips to "paid" sub-second after it enters a block
- Per-order dedicated addresses — reconciliation is 1:1 by construction, and a plain exchange withdrawal (Binance, Coinbase) is a valid payment, no wallet app needed
- Non-custodial atomic settlement — the flat 1% fee splits on-chain and the rest lands in your wallet inside the same flow; there's no platform balance and no payout step
- The
payment.settledwebhook — signed, retried, replayable; it fires only after money has actually reached your wallet - Partial payments and late payments handled and surfaced, not silently lost
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.