Transfer Requests
Create simple payment URLs for SOL and SPL token transfers
Transfer Requests
Transfer requests are the simplest form of Solana Pay integration. They create non-interactive payment URLs that wallets can use to directly compose and send transactions without requiring a server.
Overview
A transfer request URL follows this format:
solana:<recipient>
?amount=<amount>
&spl-token=<spl-token>
&reference=<reference>
&label=<label>
&message=<message>
&memo=<memo>Basic SOL Transfer
Create a simple SOL payment request:
import { address } from '@solana/kit';
import { encodeURL } from '@solana/pay';
// Recipient's Solana wallet address
const recipient = address('FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB');
// Create the payment URL
const url = encodeURL({
recipient,
amount: 0.01, // 0.01 SOL
label: 'My Store',
message: 'Thanks for your purchase!',
});
console.log(url.toString());
// Output: solana:FvJ8k8Hh...?amount=0.01&label=My%20Store&message=Thanks%20for%20your%20purchase!SPL Token Transfer
Create a USDC payment request:
import { address } from '@solana/kit';
import { encodeURL } from '@solana/pay';
// USDC mint address on mainnet
const usdcMint = address('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
const url = encodeURL({
recipient: address('FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB'),
amount: 10.50, // $10.50 USDC
splToken: usdcMint,
label: 'Coffee Shop',
message: 'Grande Latte + Tip',
});
console.log(url.toString());URL Parameters
Required Parameters
Recipient
The recipient's Solana wallet address (base58-encoded):
const recipient = address('FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB');Optional Parameters
Amount
Payment amount in user-friendly units (SOL or token units):
// SOL amounts
const solAmount = 1.5; // 1.5 SOL
const smallAmount = 0.001; // 0.001 SOL
// Token amounts (USDC example)
const usdcAmount = 25.99; // $25.99 USDCIf omitted, the wallet will prompt the user to enter the amount.
SPL Token
For token transfers, specify the mint address:
// Common SPL tokens
const USDC = address('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
const USDT = address('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB');
// Omit splToken for native SOL transfersReference
Unique identifiers for tracking payments. Generate an address to use as a reference key:
import { generateKeyPair, getAddressFromPublicKey } from '@solana/kit';
// Generate a unique reference
const keypair = await generateKeyPair();
const reference = await getAddressFromPublicKey(keypair.publicKey);Label
Merchant or app name displayed to users:
const label = 'Acme Coffee Shop';Message
Description of the payment:
const message = 'Order #12345 - 2x Americano';Memo
On-chain memo (publicly visible):
const memo = 'OrderId:12345';QR Code Integration
Generate QR codes using the built-in createQR function:
import { address } from '@solana/kit';
import { encodeURL, createQR } from '@solana/pay';
const url = encodeURL({
recipient: address('FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB'),
amount: 0.05,
label: 'Donation',
message: 'Support our project',
});
// Generate a QR code element
const qr = createQR(url);
// Append to DOM
const element = document.getElementById('qr-code');
qr.append(element);Payment Validation
Using the Merchant Client
The simplest way to find and validate payments:
import { address } from '@solana/kit';
import { createMerchantClient } from '@solana/pay';
const merchant = createMerchantClient({
rpcUrl: 'https://api.mainnet-beta.solana.com',
});
const recipient = address('FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB');
const reference = address('YOUR_REFERENCE_ADDRESS');
// Find the transaction by reference
const found = await merchant.pay.findReference(reference);
// Validate the transfer matches expected fields
await merchant.pay.validateTransfer(found.signature, {
recipient,
amount: 0.1,
});
console.log('Payment confirmed!', found.signature);Using Standalone Functions
import { createSolanaRpc, address } from '@solana/kit';
import { findReference, validateTransfer } from '@solana/pay';
const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const reference = address('YOUR_REFERENCE_ADDRESS');
// Find transaction by reference
const found = await findReference(rpc, reference);
// Validate transaction details
await validateTransfer(rpc, found.signature, {
recipient: address('FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB'),
amount: 0.1,
});
console.log('Payment confirmed!', found.signature);Watching for Payments (WebSocket)
The recommended approach — watchReference subscribes via WebSocket and resolves as soon as a matching transaction lands. No polling needed.
async function waitForPayment(reference, recipient, amount) {
const merchant = createMerchantClient({
rpcUrl: 'https://api.mainnet-beta.solana.com',
});
// Resolves on the first transaction mentioning this reference
const result = await merchant.pay.watchReference(reference);
if (result.err) {
throw new Error(`Transaction failed: ${JSON.stringify(result.err)}`);
}
// Validate that the transaction matches the expected payment
await merchant.pay.validateTransfer(result.signature, { recipient, amount });
return { signature: result.signature };
}Polling for Payments (HTTP fallback)
If WebSocket subscriptions aren't available (e.g. some RPC providers), fall back to polling with findReference:
async function pollForPayment(reference, recipient, amount) {
const merchant = createMerchantClient({
rpcUrl: 'https://api.mainnet-beta.solana.com',
});
const checkPayment = async () => {
try {
const found = await merchant.pay.findReference(reference);
await merchant.pay.validateTransfer(found.signature, { recipient, amount });
return { success: true, signature: found.signature };
} catch {
return { success: false };
}
};
// Check every 2 seconds
return new Promise((resolve) => {
const interval = setInterval(async () => {
const result = await checkPayment();
if (result.success) {
clearInterval(interval);
resolve(result);
}
}, 2000);
});
}Complete Example
Here's a complete example with QR code generation and payment monitoring:
import { address, generateKeyPair, getAddressFromPublicKey } from '@solana/kit';
import { encodeURL, createQR, createMerchantClient } from '@solana/pay';
const recipient = address('FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB');
const amount = 0.1;
// Generate unique reference for this payment
const keypair = await generateKeyPair();
const reference = await getAddressFromPublicKey(keypair.publicKey);
// Create payment URL and QR code
const url = encodeURL({ recipient, amount, reference, label: 'Demo Store', message: 'Test Purchase' });
const qr = createQR(url);
qr.append(document.getElementById('qr-code'));
// Monitor for payment
const merchant = createMerchantClient({
rpcUrl: 'https://api.mainnet-beta.solana.com',
});
const interval = setInterval(async () => {
try {
const found = await merchant.pay.findReference(reference);
await merchant.pay.validateTransfer(found.signature, { recipient, amount, reference });
clearInterval(interval);
console.log('Payment confirmed!', found.signature);
} catch {
// Payment not found yet, keep polling
}
}, 2000);Best Practices
Security
- Always validate payments server-side
- Use unique references for each payment
- Verify transaction amounts and recipients
- Check for transaction success status
User Experience
- Show clear payment amounts and descriptions
- Provide payment status updates
- Handle wallet connection gracefully
- Support both QR codes and direct links
Error Handling
- Handle wallet connection failures
- Provide fallback for unsupported wallets
- Show helpful error messages
- Implement payment timeouts
Common Use Cases
Point of Sale
const posPayment = encodeURL({
recipient: merchantWallet,
amount: 15.99,
label: 'Local Coffee Shop',
message: `Receipt #${receiptNumber}`,
memo: `POS-${terminalId}-${Date.now()}`,
});Donations
// Flexible donation amount (user enters amount in wallet)
const donationUrl = encodeURL({
recipient: charityWallet,
label: 'Save the Ocean',
message: 'Support marine conservation',
});E-commerce Checkout
const checkoutUrl = encodeURL({
recipient: storeWallet,
amount: cartTotal,
reference: orderReference,
label: storeName,
message: `Order ${orderId}`,
memo: `ORDER:${orderId}`,
});