Guide to Onramper Webhooks: Features and Best Practices
Onramper aggregates fiat-to-crypto on-ramps (and off-ramps) behind one widget and API: a wallet or dApp embeds Onramper, and Onramper routes each purchase to a provider such as the one shown as gatefi in its sample. The partner's app doesn't handle the card payment or the crypto transfer itself, so it relies on Onramper's transaction status webhooks to know when a purchase has been paid, completed or has failed.
This guide covers getting webhooks switched on through Onramper, the transaction snapshot and its six statuses, verifying X-Onramper-Webhook-Signature, and how to cope with what the docs leave out.
What are Onramper webhooks?
Onramper's webhooks aren't configured in a dashboard. You send your Customer Success Manager a URL and the ApiKey it should cover, and Onramper enables webhooks for that key and gives you a secret.
Each delivery is a JSON POST containing the transaction's current state: amounts, currencies, the underlying on-ramp provider and its transaction ID, the wallet address, the status and when it last changed. There's no event name; the status value tells you what has happened, and transactionType says whether it's a buy or an offramp.
Requests carry X-Onramper-Webhook-Signature, a hex HMAC-SHA256 of the body keyed with the secret Onramper issued.
Onramper webhook features
| Feature | Details |
|---|---|
| Configuration | Through your Customer Success Manager, per ApiKey |
| Signature | hex HMAC-SHA256 |
| Signature header | x-onramper-webhook-signature |
| Signed content | The request body |
| Key | Secret issued by Onramper at registration |
| Body | JSON, a flat transaction snapshot |
| Event routing | Body field status |
| Direction | transactionType: buy or offramp |
| Delivery ID | None documented |
| Retries and timeout | Not documented |
Events and payloads
status | Meaning | Typical action |
|---|---|---|
new | The transaction was created; no payment yet | Record the attempt |
pending | In progress and waiting on a further step | Show progress to the user |
paid | Payment was made, but the transaction isn't complete | Tell the user crypto is on the way |
completed | The transaction completed successfully | Confirm, credit or fulfil |
failed | It failed through an error or user action | Alert or offer a retry |
canceled | The user or the system cancelled it | Close the attempt |
Any other value should be logged and acknowledged. Onramper doesn't say whether every status change produces a webhook or only some, so don't build logic that assumes you'll see each step.
Payload Structure
The snapshot includes transactionId (Onramper's ID), onramp and onrampTransactionId (the provider and its reference), inAmount and sourceCurrency, outAmount and targetCurrency, paymentMethod, walletAddress, transactionHash once the transfer is on-chain, and partnerContext, the value you passed when launching the widget. The property table also lists partnerFee, which the example doesn't include.
Since every delivery is a snapshot rather than a discrete event, deduplicate on transactionId plus status and statusDate.
Published payload example
A pending card purchase of SOL, from the webhook samples library. transactionHash stays empty until the crypto is sent.
{
"apiKey": "pk_test_01K4MF4ZBPGDCV3GFFNA684NT0",
"country": "us",
"inAmount": 100,
"onramp": "gatefi",
"onrampTransactionId": "8bf94c80-test-aabb-851-143835984d1d",
"outAmount": 3.83527521,
"paymentMethod": "creditcard",
"partnerContext": "",
"sourceCurrency": "usd",
"status": "pending",
"statusDate": "2023-08-09T13:15:18.725Z",
"targetCurrency": "sol",
"transactionId": "01H7D547TESTV2RQJ52ZAB7WF7",
"transactionType": "buy",
"transactionHash": "",
"walletAddress": "testG15oy66q7cU6aNige54PxLLEfGZvRsAADjbF7D4"
}
Setting up Onramper webhooks
Prerequisites
- An Onramper partner account and ApiKey
- A public HTTPS endpoint, or a Hookdeck source URL
Get Webhooks Enabled and Your Secret
Onramper turns on webhooks through your Customer Success Manager. Send them the Hookdeck URL and paste the secret key they issue.
- Decide on the endpoint URL: your server's
/webhooks/onramperroute or the URL of an Onramper source in Hookdeck. - Send it, with your ApiKey, to your Onramper Customer Success Manager and ask for webhooks to be enabled.
- Onramper issues a secret key for that registration.
- Store it as
WEBHOOK_SECRET, used as UTF-8 bytes without decoding.
Changing the URL later also goes through your CSM, which is a good reason to register a stable URL such as a Hookdeck source.
Test a delivery
Start a local receiver and forward requests with the Hookdeck CLI:
npx hookdeck-cli listen 3000 onramper --path /webhooks/onramper
Because enabling webhooks goes through your CSM, register the Hookdeck source URL the CLI prints once, then run test purchases with a pk_test_ key whenever you need deliveries forwarded to your machine.
Securing Onramper webhooks
Compute HMAC-SHA256 of the body as received, using the secret, hex-encode it, and compare it with X-Onramper-Webhook-Signature. Onramper's TypeScript sample compares with ===; use a constant-time comparison instead. See Security and payload validation.
const crypto = require('node:crypto');
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const signature = headers['x-onramper-webhook-signature'];
if (typeof signature !== 'string') return false;
const key = Buffer.from(secret, 'utf8');
const message = rawBody;
const candidates = [signature];
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
const received = Buffer.from(candidate, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The handler keeps the latest status per transaction and only fulfils once, on completed.
const express = require('express');
function createOnramperApp({ seenSnapshot, transactions }) {
const app = express();
app.post('/webhooks/onramper', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
let tx;
try {
tx = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (!tx || typeof tx !== 'object' || Array.isArray(tx)) return res.sendStatus(400);
if (!tx.transactionId || typeof tx.status !== 'string') return res.sendStatus(400);
try {
if (await seenSnapshot(`${tx.transactionId}:${tx.status}:${tx.statusDate}`)) {
return res.sendStatus(200);
}
await transactions.recordStatus(tx.transactionId, tx.status, tx.statusDate, tx.partnerContext);
if (tx.status === 'completed') {
await transactions.fulfilOnce(tx.transactionId, {
direction: tx.transactionType,
amount: tx.outAmount,
currency: tx.targetCurrency,
hash: tx.transactionHash,
});
} else if (tx.status === 'failed' || tx.status === 'canceled') {
await transactions.close(tx.transactionId, tx.status);
}
} catch {
return res.sendStatus(503);
}
return res.sendStatus(200);
});
return app;
}
Only the body is signed, with no timestamp, so a captured snapshot can be replayed. The dedup key and fulfilOnce stop a repeat from crediting a user twice.
Hookdeck's Onramper source type verifies X-Onramper-Webhook-Signature against the raw body using the secret Onramper gave you. Onramper always receives 200; requests that fail the check are recorded as failed verification and aren't forwarded. No timestamp tolerance applies.
Make Onramper webhooks production-ready. Hookdeck Event Gateway gives Onramper a stable URL to register once, verifies every transaction update, and stores each one so you can replay it.
Onramper webhook limitations and pain points
No documented delivery guarantees
The Problem: You can't tell whether a missed status update will be resent.
Why It Happens: Onramper documents no timeout, expected response or retry policy.
Workarounds:
- Return
2xxquickly after storing the snapshot. - Flag transactions that stay open longer than expected and follow them up.
How Hookdeck Can Help: Hookdeck stores each update on arrival and retries delivery to your service, so your own outages don't depend on Onramper's undocumented behaviour.
Not self-serve
The Problem: Changing the endpoint or rotating the secret needs a request to your CSM.
Why It Happens: Webhooks are enabled per ApiKey by Onramper's team, and no rotation process is documented.
Workarounds:
- Register a stable URL that won't need changing.
- Keep the secret in a secret manager so a rotation is a single update.
How Hookdeck Can Help: A Hookdeck source URL stays the same when your infrastructure changes.
Snapshots without event IDs
The Problem: Duplicate or out-of-order snapshots move a transaction backwards or double-credit it.
Why It Happens: Each delivery is the current state with no delivery ID, and ordering isn't documented.
Workarounds:
- Deduplicate on
transactionId,statusandstatusDate. - Ignore a snapshot whose
statusDateis older than the one you hold.
How Hookdeck Can Help: Hookdeck deduplication can use those body fields to discard repeats before they reach you.
Unclear triggers
The Problem: A user waits on a paid update that never comes.
Why It Happens: The docs describe what each status means but not which changes send a webhook.
Workarounds:
- Treat intermediate statuses as optional and key important actions on
completed,failedandcanceled.
How Hookdeck Can Help: Hookdeck's event history shows exactly which statuses your integration receives for each transaction.
Best practices
Use partnerContext to link transactions
Pass your own order or user reference as partnerContext when launching Onramper, and look it up from the webhook rather than matching on wallet addresses.
Compare signatures in constant time
Onramper's sample uses ===. Use crypto.timingSafeEqual or hmac.compare_digest on decoded bytes.
Keep test and live apart
The sample uses a pk_test_ key. Check apiKey in the body and route test transactions away from production fulfilment.
Acknowledge quickly and process idempotently
Store, acknowledge, then act, following our asynchronous processing guide, and record each snapshot key with its effect as described in implementing webhook idempotency.
Conclusion
Onramper's webhooks are a stream of transaction snapshots: one shape, a status that changes, and a straightforward HMAC. Getting them enabled goes through your CSM, and the docs say nothing about retries, triggers or ordering, so build around completed, deduplicate snapshots and reconcile open transactions.
Hookdeck Event Gateway gives you a fixed URL to hand to Onramper and a stored, verifiable record of every update.
Get started with Hookdeck for free and handle Onramper webhooks reliably in minutes.