Webhooks
Overview
Webhooks in Ramp Network 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 Network.
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 rampNetworkKey = readFileSync('ramp-network.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)),
rampNetworkKey,
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-network.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.