Skip to main content



Webhooks in Ramp are very simple. If you pass a proper webhook URL during widget initialization, we will call this URL for every major status change of the purchase initialized in that widget instance. Each call to this URL will contain details about the purchase and its state change history.

Retry policy

If your server fails to receive our webhook call, we retry four times with a delay of 3 minutes. After that, no attempts to resend webhook calls will be made.

Available webhooks


This webhook is called for every major state update of the purchase. It will be executed using HTTP POST and will contain JSON data with a RampPurchase object inside:

interface PurchaseStatusWebhookEvent {
purchase: RampPurchase;

The RampPurchase object is the same as in the SDK purchase status events. You can learn more about how to use webhooks here.


This webhook is called for every major state update of the sale. It will be executed using HTTP POST and will contain JSON data with a RampSale object inside:

interface SaleStatusWebhookEvent {
mode: 'OFFRAMP';
payload: RampSale;

The RampSale object is the same as in the SDK purchase status events. You can learn more about how to use webhooks here.

Example Using express.js

A simple express.js application:

const express = require('express'),
bodyParser = require('body-parser'),
app = express();

app.use(bodyParser.json());'/', function (request, response) {
const event = request.body;
const sale = event.purchase;





# Run `ngrok http 3000` first to pass internet traffic to this simple application
$ nodejs app.js
{ type: 'CREATED',
purchase: {
id: "322",
endTime: null,
asset: {
address: null,
symbol: 'ETH',
chain: 'ETH',
name: 'Ether',
decimals: 18,
type: "ETH"
cryptoAmount: '30000000000000000',
fiatCurrency: 'GBP',
fiatValue: 0.05,
assetExchangeRate: 1.29518206550279,
assetExchangeRateEur: 1.51277630353432,
baseRampFee: 0.09892653257425743,
networkFee: 0.008420210000000001,
appliedFee: 0.10734674257425743,
purchaseViewToken: "...",
receiverAddress: "0x2222222222222222222222222222222222222222",
createdAt: "2019-10-24T14:41:39.372Z",
updatedAt: "2019-10-24T15:41:41.065Z",
status: "INITIALIZED",
finalTxHash: null,
paymentMethodType: "MANUAL_BANK_TRANSFER"


For testing we recommend ngrok.

It creates a unique domain that will redirect traffic to your localhost. You don't need to have any HTTP servers running on localhost -- ngrok allows you to inspect all calls to your unique domain on http://localhost:4041/

Securing Webhooks

For your safety we sign our every webhook call with a private ECDSA key.

Before you parse body of the received request check the X-Body-Signature header to verify if the message is signed by Ramp.

For your safety we sign our webhook calls. The messages are signed using an ECDSA key and a sha256 digest.

To create a message to sign, we serialize the original request body (JSON) with fast-json-stable-stringify module, which sorts the object keys before serialization, creating a deterministic output.

Then, we sign the message using an ECDSA key (based on secp256k1 curve) with a sha256 digest function.

The signature is finally encoded to base64 and sent with the original request in the X-Body-Signature header.

Use our public key to verify incoming POST calls. For example using nodejs crypto.verify function:

crypto.verify('sha256', Buffer.from(message), rampPublicKey, Buffer.from(signature, 'base64'));

Production public key:

-----END PUBLIC KEY-----

Demo environment public key:

-----END PUBLIC KEY-----


  • X-Body-Signature is a base64 representation of the signature (in DER format), that's why you have to provide 'base64' as the second argument to Buffer.from() call

  • The message we sign is a stringified request body (JSON) with its keys in the alphabetical order and without whitespaces

  • We use ECDSA key and sha256 hash function to sign the message

Example: node.js

import { json } from 'body-parser';
import { verify } from 'crypto';
import express from 'express';
import stableStringify from 'fast-json-stable-stringify';
import { readFileSync } from 'fs';

const rampKey = readFileSync('ramp-public.pem').toString();

const app = express();

app.use(json());'/', (req, res) => {
if (req.body && req.header('X-Body-Signature')) {
const verified = verify(
Buffer.from(req.header('X-Body-Signature'), 'base64'),

if (verified) {
} else {
console.error('ERROR: Invalid signature');
} else {
console.error('ERROR: Wrong request structure');


Example: Python Flask

import base64
import hashlib
import json
import pprint

from ecdsa import VerifyingKey
from ecdsa.util import sha256, sigdecode_der
from flask import Flask, request

with open("ramp-public.pem", "rb") as f:
vk = VerifyingKey.from_pem(

app = Flask(__name__)

@app.route("/", methods=["POST"])
def verifier_app():

if not "X-Body-Signature" in request.headers or not request.json:
return "ERROR: Wrong request structure", 401

sig_base64 = request.headers["X-Body-Signature"]
sig_bytes = base64.b64decode(sig_base64)

stringified_message = json.dumps(
sort_keys=True, # THESE
indent=None, # ARE
separators=(",", ":") # IMPORTANT
message_bytes = stringified_message.encode("utf-8")

vk.verify(sig_bytes, message_bytes,
hashfunc=sha256, sigdecode=sigdecode_der)
return 'ERROR: Invalid signature', 401

return f"Received a verified message!\n{request.json}"

Passing custom parameters to webhooks

We don't send user details in webhooks because of privacy policy restrictions. If you need to send some custom parameters to the webhook to identify the purchase you can pass it to our SDK as a query param at the end of the webhookStatusUrl or offrampWebhookV3Url parameter. For example, you can pass a uniqueId parameter like this when redirecting your user to the widget and the webhook URL will be called using the query params you provided.