Gareth Wilson Gareth Wilson

Guide to Onramper Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationThrough your Customer Success Manager, per ApiKey
Signaturehex HMAC-SHA256
Signature headerx-onramper-webhook-signature
Signed contentThe request body
KeySecret issued by Onramper at registration
BodyJSON, a flat transaction snapshot
Event routingBody field status
DirectiontransactionType: buy or offramp
Delivery IDNone documented
Retries and timeoutNot documented

Events and payloads

statusMeaningTypical action
newThe transaction was created; no payment yetRecord the attempt
pendingIn progress and waiting on a further stepShow progress to the user
paidPayment was made, but the transaction isn't completeTell the user crypto is on the way
completedThe transaction completed successfullyConfirm, credit or fulfil
failedIt failed through an error or user actionAlert or offer a retry
canceledThe user or the system cancelled itClose 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.

  1. Decide on the endpoint URL: your server's /webhooks/onramper route or the URL of an Onramper source in Hookdeck.
  2. Send it, with your ApiKey, to your Onramper Customer Success Manager and ask for webhooks to be enabled.
  3. Onramper issues a secret key for that registration.
  4. 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 2xx quickly 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, status and statusDate.
  • Ignore a snapshot whose statusDate is 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, failed and canceled.

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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.