API: Sign requests (V2)

A V2 API signature covers the entire request envelope — method, path, query, signed headers, and body hash — not just the URL. Include the signature in request headers.

Request headers

Authorization: YOUR_API_KEY
x-onramper-signature: v2:{base64_signature}
x-onramper-timestamp: 2024-01-15T10:30:00.000Z
x-onramper-nonce: 550e8400-e29b-41d4-a716-446655440000
💡

x-onramper-timestamp and x-onramper-nonce are sent as headers but go into lines 2–3 of the canonical string — not the signed headers section (line 7).

The canonical string

The same 8-line format as widget signing, with API-specific values:

LineComponentExample
1VersionONRAMPER-SIG-V2
2Timestamp2024-01-15T10:30:00.000Z
3Nonce550e8400-e29b-41d4-...
4HTTP method (uppercase)POST
5Request path/checkout/intent
6Canonical query (sorted A–Z; empty if none)amount=100&currency=USD
7Canonical headersauthorization:YOUR_API_KEY
8Body hash (SHA256 hex)e3b0c44...

Canonical headers (line 7): only two headers are ever signed, in alphabetical order, as name:value (lowercase name, trimmed value), joined by \n:

  • authorizationalways required
  • content-type — only if present in the request

Body hash (line 8):

Body typeHandling
Empty body (e.g. GET)SHA256 of empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
JSON bodyCanonicalize per RFC 8785 (sorted keys, no whitespace), then SHA256
Non-JSON stringSHA256 of the string as-is

Securing endpoints on your side

The proposed flow is:

  1. Frontend → Backend: your frontend calls your backend.
  2. Backend: authenticate the end user, validate the request and sign the request.
  3. Backend → Onramper: send the signed request to Onramper.
  4. Backend → Frontend: return the result to your frontend.

Your frontend → backend call should always include additional controls such as:

  • User authentication (prove who is making the request)
  • Request validation / matching (e.g., the user is allowed to use the specified wallet address)
  • Rate limiting to prevent burst/abusive checkout attempts per user

This is especially important for flows that end up calling Onramper’s /checkout endpoints and we will audit your integration for this before going live.

Example: signed GET request

curl -X GET "https://api.onramper.com/quotes/usd/btc?amount=100" \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "x-onramper-signature: v2:MEUCIQDKZf3L8gT2x..." \
  -H "x-onramper-timestamp: 2024-01-15T10:30:00.000Z" \
  -H "x-onramper-nonce: 550e8400-e29b-41d4-a716-446655440000"

Complete TypeScript example

import { createHash, createPrivateKey, randomUUID, sign } from 'crypto';
import canonicalize from 'canonicalize'; // npm i canonicalize — RFC 8785

function sha256(data: string): string {
  return createHash('sha256').update(data).digest('hex');
}

function signApiRequestV2(options: {
  method: string;
  path: string;
  apiKey: string;
  privateKeyPem: string;
  queryParams?: Record<string, string>;
  body?: object;
  contentType?: string;
}): Record<string, string> {
  const { method, path, apiKey, privateKeyPem, queryParams, body, contentType } = options;

  const timestamp = new Date().toISOString();
  const nonce = randomUUID();

  const canonicalQuery = queryParams
    ? (() => { const p = new URLSearchParams(queryParams); p.sort(); return p.toString(); })()
    : '';

  const headerParts = [`authorization:${apiKey}`];
  if (contentType) headerParts.push(`content-type:${contentType}`);
  const canonicalHeaders = headerParts.join('\n');

  const bodyHash = body ? sha256(canonicalize(body) ?? '') : sha256('');

  const contentToSign = [
    'ONRAMPER-SIG-V2',
    timestamp,
    nonce,
    method.toUpperCase(),
    path,
    canonicalQuery,
    canonicalHeaders,
    bodyHash,
  ].join('\n');

  const privateKey = createPrivateKey(privateKeyPem);
  const signature = sign(null, Buffer.from(contentToSign), privateKey);

  return {
    'Authorization': apiKey,
    'x-onramper-signature': `v2:${signature.toString('base64')}`,
    'x-onramper-timestamp': timestamp,
    'x-onramper-nonce': nonce,
  };
}