Skip to main content

Webhooks

Overview

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

webhookStatusUrl

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 {
type: 'CREATED' | 'RELEASED' | 'RETURNED';
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.

offrampWebhookV3Url

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 {
type: 'CREATED' | 'RELEASED' | 'EXPIRED';
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());

app.post('/', function (request, response) {
const event = request.body;
console.log(event);
const sale = event.payload;

console.log(sale.id);
console.log(sale.fiat.amount);
console.log(sale.fiat.currencySymbol);

response.send('OK');
});

app.listen(3000);

Run:

# Run `ngrok http 3000` first to pass internet traffic to this simple application
$ nodejs app.js
{
"id": "9393916e-c3c5-46c4-9132-18106a192637",
"type": "CREATED",
"mode": "OFFRAMP",
"payload": {
"id": "70b47a42-aed2-4acb-b463-3977831ffc0d",
"saleViewToken": "she6rq5hfgtm3v2u",
"transactionHash": "0xc13d7905be5c989378a945487cd2a1193627ae606009e28e296d48ddaec66162",
"createdAt": "2024-03-21T13:47:19.344Z",
"updatedAt": "2024-03-21T13:47:19.369Z",
"exchangeRate": "1.167020468371995",
"cryptoToEurRate": "3249.9446182275124",
"fees": {
"amount": "0.85",
"currencySymbol": "GBP"
},
"crypto": {
"amount": "1640000000000000",
"assetInfo": {
"address": null,
"symbol": "ETH",
"chain": "ARBITRUM",
"type": "NATIVE",
"name": "Ethereum",
"decimals": 18
},
"status": null
},
"fiat": {
"amount": "3.71",
"currencySymbol": "GBP",
"status": "not-started",
"payoutMethod": "CARD"
}
}
}
70b47a42-aed2-4acb-b463-3977831ffc0d
3.71
GBP

Testing

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:

-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAElvxpYOhgdAmI+7oL4mABRAfM5CwLkCbZ
m64ERVKAisSulWFC3oRZom/PeyE2iXPX1ekp9UD1r+51c9TiuIHU4w==
-----END PUBLIC KEY-----

Demo environment public key:

-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEevN2PMEeIaaMkS4VIfXOqsLebj19kVeu
wWl0AnkIA6DJU0r3ixkXVhJTltycJtkDoEAYtPHfARyTofB5ZNw9xA==
-----END PUBLIC KEY-----

Important:

  • 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());

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

if (verified) {
console.log('SUCCESS');
res.status(204).send();
} else {
console.error('ERROR: Invalid signature');
res.status(401).send();
}
} else {
console.error('ERROR: Wrong request structure');
res.status(401).send();
}
});

app.listen(3000);

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(f.read())

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(
request.json,
sort_keys=True, # THESE
indent=None, # ARE
separators=(",", ":") # IMPORTANT
)
message_bytes = stringified_message.encode("utf-8")

try:
vk.verify(sig_bytes, message_bytes,
hashfunc=sha256, sigdecode=sigdecode_der)
except:
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 https://example.com/webhook?uniqueId=123 when redirecting your user to the widget and the webhook URL will be called using the query params you provided.