Checkout V2
Create a checkout session with the end user's IP hash and platform declared. Requires a Signature V2 request.
Checkout V2 requires a V2 signatureEvery
POST /checkout/v2/intentcall must be signed with your Ed25519 key. There is no API-key-only mode on this endpoint: an unsigned or invalid request returns 401. See API: Sign requests (V2).
V1 is being deprecated
POST /checkout/intentstill works, but it will be deprecated soon. Build new integrations on V2, and plan the migration if you are already live on V1.
Checkout V2 is the same checkout call with two extra fields:
clientIpHash: SHA256 of the end user's IP, hashed on your backend. We check the IP of the browser that opens the session against it, so a leaked session link is useless from another machine. Required.platform: where your integration runs. Providers that only serve whitelisted platforms are dropped when you leave it out.
The flow
- The user starts a purchase in your app.
- Your backend takes the user's IP, hashes it with SHA256, and calls
POST /checkout/v2/intentwith a V2 signature. - Onramper returns
redirectUrlandsessionId. - You send the user's browser to
redirectUrl. - Onramper checks the IP behind that browser against
clientIpHash, then takes the user to the provider. A mismatch returns 403.
Step 1: add platform to your quotes call
GET /quotes/usd/btc?amount=100&paymentMethod=creditcard&platform=mobile-ios
| Value | Where your integration runs | Example |
|---|---|---|
web | Web browser, desktop or mobile | React app in Chrome or Safari |
mobile-ios | Native iOS app | Swift or SwiftUI app |
mobile-android | Native Android app | Kotlin or Java app |
webview-ios | WebView inside an iOS app | React Native WebView on iOS |
webview-android | WebView inside an Android app | React Native WebView on Android |
browser-extension | Browser extension | Chrome, Firefox, Edge |
desktop | Native desktop app | Electron app |
Pick the value that describes where the user actually is. A React Native WebView on iOS is webview-ios, not web.
Leaving platform out is not neutral: providers that require whitelisting disappear from quotes and cannot be used at checkout. Use the same value in the checkout body.
Step 2: call POST /checkout/v2/intent
Same body as POST /checkout/intent, plus:
| Field | Required | Value |
|---|---|---|
clientIpHash | Yes | 64-character hex SHA256 of the end user's IP. Missing or malformed returns 400. |
platform | Recommended | The same value you sent to quotes. |
Both fields sit inside the signed body, so build them before you sign.
| Header | Value |
|---|---|
Authorization | Your API key |
x-onramper-signature | v2:{base64_signature} |
x-onramper-timestamp | ISO 8601 timestamp |
x-onramper-nonce | UUID v4 |
Response:
{
"redirectUrl": "https://buy.onramper.com?session=01ABC...&resumeToken=8f2c...",
"sessionId": "01ABC...",
"resumeToken": "8f2c..."
}Step 3: send the user to redirectUrl
Redirect the browser to redirectUrl exactly as returned. It carries a one-time resume token, so treat it as a credential: do not log it, store it, or share it anywhere else. Each session is single-use and expires 10 minutes after intent.
Errors
At intent, on your backend:
| Status | Cause |
|---|---|
| 400 | clientIpHash is missing or is not a 64-character hex hash |
| 401 | Signature missing, expired, replayed or invalid (errorId 4011) |
After the redirect, when the user's browser opens the session:
| Status | Code | Cause |
|---|---|---|
| 403 | IP_MISMATCH | The IP opening the session does not match clientIpHash |
| 404 | SESSION_NOT_FOUND | Unknown session |
| 409 | SESSION_CONSUMED | Session already used |
| 410 | SESSION_EXPIRED | More than 10 minutes since intent |
Example
Signing uses the signApiRequestV2 helper from API: Sign requests (V2).
const API_BASE = 'https://api.onramper.com';
const PLATFORM = 'mobile-ios'; // match your integration type
function sha256(data: string): string {
return createHash('sha256').update(data).digest('hex');
}
async function createCheckout(userIp: string, onramp: string) {
const body = {
onramp,
source: 'eur',
destination: 'btc',
amount: 300,
type: 'buy',
country: 'de',
paymentMethod: 'creditcard',
network: 'bitcoin',
wallet: { address: 'bc1q...' },
clientIpHash: sha256(userIp),
platform: PLATFORM,
};
const resp = await fetch(`${API_BASE}/checkout/v2/intent`, {
method: 'POST',
headers: {
...signApiRequestV2({
method: 'POST',
path: '/checkout/v2/intent',
body,
contentType: 'application/json',
}),
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const { redirectUrl } = await resp.json();
return redirectUrl; // send the user's browser here
}Hash the end user's IP, not your server's. Read it from the connection your frontend makes to your backend.
Migrating from V1
| Step | Change |
|---|---|
| 1 | Add platform to your quotes request |
| 2 | Point checkout at /checkout/v2/intent |
| 3 | Add clientIpHash to the checkout body |
| 4 | Add platform to the checkout body, the same value as quotes |
| 5 | Redirect to redirectUrl from the response instead of building a URL yourself |
If you already sign with V2, the signing format does not change. Only the path, the two new fields and the response shape differ.
Updated about 2 hours ago

