Authentication
Every Onramp endpoint that touches a customer is authorized by a signature of the customer's Rubin key. There are no API keys: the address in the request is the customer, and only the holder of that address's key can create orders whose USDC is credited to it.
Public endpoints — GET /health and GET /v4/sbp/rates — need no signature. (If a customer's flow has ratesPerCustomer: true, sign GET /v4/sbp/rates as well.)
Headers
| Header | Required | Value |
|---|---|---|
x-onramp-address | always | Customer address, bech32 rit1… (lowercase, 20-byte payload) |
x-onramp-timestamp | always | Unix time in seconds |
x-onramp-signature | always | Signature of the message below; encoding depends on the wallet type |
x-onramp-wallet-type | no (default cosmos) | cosmos, keplr, evm or session — see Signature schemes |
x-onramp-pubkey | for cosmos and keplr | Base64 of the 33-byte compressed secp256k1 public key |
Content-Type | for requests with a body | application/json |
Signed message
RITBIT_ONRAMP:{METHOD}:{path}:{bodyHash}:{timestamp}| Part | Rule |
|---|---|
METHOD | Uppercase HTTP method: GET or POST |
path | Full request path including /v4 and without the query string, e.g. /v4/sbp/orders |
bodyHash | Lowercase hex SHA-256 of the exact body bytes you send; empty string for GET and for requests without a body |
timestamp | The same value as x-onramp-timestamp |
Examples:
RITBIT_ONRAMP:GET:/v4/sbp/customer::1791200000
RITBIT_ONRAMP:POST:/v4/sbp/orders:3b7a…e91c:1791200000Validity window
The server accepts a timestamp within ±120 seconds of its own clock. There is no nonce: the same signature stays valid for the same method, path and body for the whole window, so a polling client may reuse one signature for repeated GET requests. Sign every POST afresh.
Signature schemes
x-onramp-wallet-type | Who signs | Use it when |
|---|---|---|
cosmos | A secp256k1 key held by your backend or app | Recommended for partner integrations — see below |
evm | An EVM key or wallet, EIP-712 typed data | You already hold EVM keys, or the user signs in an EVM wallet — see below |
keplr | Keplr wallet, ADR-36 signArbitrary | The end user connects a Keplr wallet |
session | On-chain trading keys | Used by the rubin.trade web app; contact us before relying on it |
cosmos scheme
- Build the signed message as above.
- Compute
digest = SHA-256(message + ':ONRAMP":' + timestamp). Note the literal double quote afterONRAMP— the string is{message}:ONRAMP":{timestamp}. - Sign
digestwith secp256k1. Send the signature as base64 of the 65-byte fixed-length formr ‖ s ‖ recovery. - Send the compressed public key in
x-onramp-pubkey. The server derivesbech32("rit", ripemd160(sha256(pubkey)))and rejects the request if it differs fromx-onramp-address.
A partner that serves users without their own Rubin wallets keeps one key per end user and uses that key's address as the customer. Each user then has their own verification, limits and order history, and the USDC is credited to their own address.
Example (TypeScript)
import { ripemd160, Secp256k1, sha256 } from '@cosmjs/crypto';
import { toBech32, toHex } from '@cosmjs/encoding';
const BASE_URL = 'https://onramp.testnet.rubin.trade';
const enc = new TextEncoder();
export async function onrampRequest(
privateKey: Uint8Array,
method: 'GET' | 'POST',
pathWithQuery: string,
body?: unknown,
) {
const { pubkey } = await Secp256k1.makeKeypair(privateKey);
const compressed = Secp256k1.compressPubkey(pubkey);
const address = toBech32('rit', ripemd160(sha256(compressed)));
const rawBody = body === undefined ? '' : JSON.stringify(body);
const bodyHash = rawBody === '' ? '' : toHex(sha256(enc.encode(rawBody)));
const timestamp = Math.floor(Date.now() / 1000);
const path = pathWithQuery.split('?')[0];
const message = `RITBIT_ONRAMP:${method}:${path}:${bodyHash}:${timestamp}`;
const digest = sha256(enc.encode(`${message}:ONRAMP":${timestamp}`));
const signature = await Secp256k1.createSignature(digest, privateKey);
const res = await fetch(`${BASE_URL}${pathWithQuery}`, {
method,
headers: {
'x-onramp-address': address,
'x-onramp-timestamp': String(timestamp),
'x-onramp-wallet-type': 'cosmos',
'x-onramp-pubkey': Buffer.from(compressed).toString('base64'),
'x-onramp-signature': Buffer.from(signature.toFixedLength()).toString('base64'),
...(rawBody === '' ? {} : { 'Content-Type': 'application/json' }),
},
body: rawBody === '' ? undefined : rawBody,
});
const data = await res.json();
if (!res.ok) throw Object.assign(new Error(data.errors?.[0]?.msg), { status: res.status, data });
return data;
}
// const customer = await onrampRequest(key, 'GET', '/v4/sbp/customer');evm scheme
A Rubin address and an EVM address are two encodings of the same 20 bytes: rit1… is bech32("rit", bytes), 0x… is their hex. An EVM key therefore owns the Rubin address of the same bytes and can sign for it directly.
-
Build the signed message as above.
-
Sign EIP-712 typed data with exactly this domain and type — any difference changes the digest:
Field Value domain { name: 'Rubin', version: '1' }— nochainId, noverifyingContractprimary type AccountAction(string action,string message,uint64 timestamp)actionONRAMPmessageThe signed message from step 1 timestampThe same value as x-onramp-timestamp -
Send the 65-byte signature as
0x-prefixed hex inx-onramp-signature. Nox-onramp-pubkey: the server recovers the signer from the signature and compares its 20 bytes withx-onramp-address.
The domain names no chain, so a wallet signs it on whatever network it is connected to.
import { toBech32 } from '@cosmjs/encoding';
import { hexToBytes } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(evmPrivateKey);
const address = toBech32('rit', hexToBytes(account.address)); // x-onramp-address
// message and timestamp built exactly as in the cosmos example above
const signature = await account.signTypedData({
domain: { name: 'Rubin', version: '1' },
types: {
AccountAction: [
{ name: 'action', type: 'string' },
{ name: 'message', type: 'string' },
{ name: 'timestamp', type: 'uint64' },
],
},
primaryType: 'AccountAction',
message: { action: 'ONRAMP', message, timestamp: BigInt(timestamp) },
});
// headers: x-onramp-address, x-onramp-timestamp,
// x-onramp-wallet-type: 'evm', x-onramp-signature: signatureAuthentication errors
| HTTP | Message | Cause |
|---|---|---|
401 | Missing onramp auth headers | No address, signature or timestamp header |
400 | Invalid onramp auth address | The address is not a valid rit1… address |
401 | Invalid onramp auth timestamp | The timestamp is not a number |
400 | Timestamp is not within the valid range of 120 seconds | Clock skew or a stale signature |
400 | Address … does not correspond to the pubkey provided … | x-onramp-pubkey does not hash to the address |
400 | Address … does not correspond to the signer … | evm: the recovered signer is another address — usually a wrong domain, type or message |
400 | Signature verification failed | Wrong message, wrong digest or a re-serialized body |
Errors use the common error format.