Widget: Sign a URL (V2)
When your widget URL includes wallet addresses or other sensitive parameters, you must sign the URL server-side with V2 and append the signature parameters. If the signature is invalid, missing, or expired, the transaction is restricted at checkout.
V2 URL parameters
| Parameter | Required | Description | Format |
|---|---|---|---|
sigV2 | Yes | Ed25519 signature | v2:{base64_signature} |
sigV2Timestamp | Yes | When the URL was signed | ISO8601 with Z suffix |
sigV2Nonce | Yes | Replay protection | UUID v4 |
sigV2Fields | Recommended | Comma-separated list of signed fields | e.g. apiKey,wallets |
sigV2Expiry | No | When the signed URL expires | ISO8601, max 30 minutes |
IfsigV2Fieldsis omitted, onlyapiKey,wallets, andnetworkWalletsare assumed signed. If you sign any other fields (e.g.defaultAmount), you must list them insigV2Fields.
Core parameters present in the URL but not listed insigV2Fieldsare silently removed by the server. For example,[email protected]withoutsigV2Fieldswill be stripped and ignored.
Which parameters must be signed
All core parameters must be signed when present in your URL. These include apiKey (always required), wallets, networkWallets, walletAddressTags, defaultAmount, defaultFiat, defaultCrypto, defaultPaymentMethod, onlyOnramps, mode, successRedirectUrl, failureRedirectUrl, partnerContext, redirectAtCheckout, redirectTo, uniqueUserId, email, kycProvider, kycToken, onlyCryptos, onlyCryptoNetworks, onlyFiats, excludeCryptos, excludeCryptoNetworks, excludeFiats, excludePaymentMethods, isAddressEditable, isAmountEditable, enableAuth, and endUserIpHash.
Theme and display parameters (themeName, primaryColor, borderRadius, txnAmount, etc.) do not need to be signed.
Bind the session to the end user's IP
Optional. Add endUserIpHash to the URL and list it in sigV2Fields to lock the checkout session to one machine. If the link leaks, it is useless from any other IP.
| Requirement | Detail |
|---|---|
| Value | 64-character lowercase hex SHA256 of the end user's IP. Uppercase hex is rejected. |
| Signature | Signature V2 only. On a V1-signed URL the parameter is stripped and the session is unbound. |
| Where to hash | Your backend, from the connection your frontend opens to it. Not your own server's IP. |
| On mismatch | The user gets a 403 when the widget reaches checkout. |
const widgetUrl = signWidgetUrlV2({
baseUrl: 'https://buy.onramper.com',
privateKeyPem: process.env.PRIVATE_KEY!,
fields: {
apiKey: 'YOUR_API_KEY',
wallets: 'btc:bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
endUserIpHash: sha256(userIp),
},
});Leave it out if your users are on rotating IPs, such as mobile networks or corporate VPNs. The IP has to be the same at signing time and at checkout, or they are blocked mid-flow.
Server-side integrations get the same binding through the checkout API: see Checkout V2.
How to generate the signature
1. Build the canonical string
V2 signs an 8-line string joined by newline characters (\n). For widget URLs:
| Line | Value | Notes |
|---|---|---|
| 1 | ONRAMPER-SIG-V2 | Fixed version prefix |
| 2 | Timestamp | ISO8601 with milliseconds, e.g. 2024-01-15T10:00:00.000Z |
| 3 | Nonce | Random UUID v4, fresh per request |
| 4 | GET | Always GET for widget URLs |
| 5 | / | Always root path |
| 6 | Canonical query | Signed fields only, URL-encoded, sorted A–Z |
| 7 | (empty line) | No headers for widget URLs |
| 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 | SHA256 of empty string (no body) |
Example canonical string for apiKey=pk_test_xxx, wallets=btc:bc1qxy..., defaultAmount=100:
ONRAMPER-SIG-V2
2024-01-15T10:00:00.000Z
550e8400-e29b-41d4-a716-446655440000
GET
/
apiKey=pk_test_xxx&defaultAmount=100&wallets=btc%3Abc1qxy...
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
The canonical query must useapplication/x-www-form-urlencodedencoding (colons become%3A, spaces become+). UseURLSearchParamswith.sort()in JavaScript — it matches our server-side verification exactly. Note this differs from V1, which signed unencoded values.
2. Sign with Ed25519 and prefix with v2:
Sign the canonical string with your private key, base64-encode the result, and prefix it with exactly v2: (three characters).
3. Append signature parameters to the URL
Complete TypeScript example
import { createHash, createPrivateKey, randomUUID, sign } from 'crypto';
function sha256(data: string): string {
return createHash('sha256').update(data).digest('hex');
}
interface SignWidgetUrlOptions {
baseUrl: string;
privateKeyPem: string;
fields: Record<string, string>; // All signed fields — apiKey MUST be included
expiryMinutes?: number;
}
function signWidgetUrlV2(options: SignWidgetUrlOptions): string {
const { baseUrl, privateKeyPem, fields, expiryMinutes = 15 } = options;
const timestamp = new Date().toISOString();
const nonce = randomUUID();
const expiry = new Date(Date.now() + expiryMinutes * 60 * 1000).toISOString();
// Canonical query: URLSearchParams matches server-side encoding exactly
const canonicalParams = new URLSearchParams(fields);
canonicalParams.sort();
const canonicalQuery = canonicalParams.toString();
// 8-line canonical string
const contentToSign = [
'ONRAMPER-SIG-V2',
timestamp,
nonce,
'GET',
'/',
canonicalQuery,
'', // No headers for widget URLs
sha256(''), // Empty body hash
].join('\n');
const privateKey = createPrivateKey(privateKeyPem);
const signature = sign(null, Buffer.from(contentToSign), privateKey);
const url = new URL(baseUrl);
for (const [key, value] of Object.entries(fields)) {
url.searchParams.set(key, value);
}
url.searchParams.set('sigV2', `v2:${signature.toString('base64')}`);
url.searchParams.set('sigV2Timestamp', timestamp);
url.searchParams.set('sigV2Nonce', nonce);
url.searchParams.set('sigV2Expiry', expiry);
url.searchParams.set('sigV2Fields', Object.keys(fields).sort().join(','));
return url.toString();
}
// Usage — apiKey MUST be included in fields
const widgetUrl = signWidgetUrlV2({
baseUrl: 'https://buy.onramper.com',
privateKeyPem: process.env.PRIVATE_KEY!,
fields: {
apiKey: 'YOUR_API_KEY',
wallets: 'btc:bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
defaultAmount: '100',
defaultFiat: 'USD',
},
});The final URL
https://buy.onramper.com
?apiKey=YOUR_API_KEY
&wallets=btc:bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
&defaultAmount=100
&sigV2=v2:MEUCIQDKZf3L8gT2x...
&sigV2Timestamp=2024-01-15T10:00:00.000Z
&sigV2Nonce=550e8400-e29b-41d4-a716-446655440000
&sigV2Expiry=2024-01-15T10:15:00.000Z
&sigV2Fields=apiKey,defaultAmount,wallets
Best practices
- Sign on the server. Anything shipped to the browser is leakable — treat client-side signing as a key compromise.
- Treat each signed URL as single-use. The nonce burns on first load, so caching or sharing a URL guarantees the next request fails.
- Generate a fresh UUID v4 nonce per request. Never reuse one.
- Keep expiry windows short. 15 minutes is the default; 30 minutes is the maximum.
- Sign every wallet parameter the user can influence:
wallets,networkWallets,walletAddressTags.
Updated 18 days ago

