Webhooks

Overview

Webhooks in Ramp Instant 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.

Headers

X-Body-Signature

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).

Production public key:

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

Staging (test) evironment public key:

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

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 Instant.

To verify the call, you first need to stringify the request body with all its keys in alphabetical order and without any whitespaces. We use fast-json-stable-stringify node.js module to do that.

You also need our public key (see above).

Then, using the header value, prepared message and our public key, you can verify the call.

For example, using nodejs crypto.verify function:

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

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 reqest.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}"

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 Purchase object inside:

interface PurchaseStatusWebhookEvent {
type: 'CREATED' | 'RELEASED' | 'RETURNED' | 'ERROR';
purchase: RampInstantPurchase;
}

The RampInstantPurchase object is the same as in the SDK purchase status events.

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 purchase = event.purchase;
console.log(purchase.id);
console.log(purchase.fiatValue);
console.log(purchase.fiatCurrency);
response.send('OK');
});
app.listen(3000);

Run:

// Run `ngrok http 3000` first to pass internet traffic to this simple application
$ nodejs app.js
{ type: 'CREATED',
purchase:
{ id: "322",
endTime: null,
tokenAddress: null,
asset: {
address: null,
symbol: 'ETH',
name: 'Ether',
decimals: 18
},
escrowAddress: null,
cryptoAmount: '30000000000000000',
ethAmount: '30000000000000000',
tokenAmount: null,
fiatCurrency: 'GBP',
fiatValue: 0.05,
assetExchangeRate: 1.29518206550279,
poolFee: 0.01,
rampFee: 0.000582831929476256,
purchaseHash: "...",
purchaseViewToken: "...",
receiverAddress: "0x2222222222222222222222222222222222222222",
actions: [ [Object], [Object] ] } }
322
0.05
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/