Skip to main content

Webhook Signature Validation

To ensure the security and integrity of webhook notifications, Ripio includes an HMAC-SHA256 signature with each request. This signature allows you to verify that the webhook payload was indeed sent by Ripio and has not been tampered with during transmission.

Overview of Signature Validation

Webhook requests from Ripio will include an Http-X-Wh-Signature-256 header. This header contains the HMAC-SHA256 signature of the request payload, generated using a shared secret key provided to you by Ripio.

Validating the Signature: Step-by-Step Guide

  1. Retrieve the Signature Header: Extract the value of the Http-X-Wh-Signature-256 header from the incoming webhook request.
  2. Read the Raw Request Body: Read the raw bytes of the webhook request body. Do not parse and re-serialize the JSON — verification must happen over the exact bytes Ripio sent.
  3. Verify the Signature: Using your shared secret key, compute the HMAC-SHA256 of the raw body bytes and compare it (after prepending sha256=) with the value in the Http-X-Wh-Signature-256 header. If they match, the webhook is authentic.

Wire Format Reference

You do not need to re-serialize the payload yourself — verify over the raw body — but knowing the format helps if you ever need to debug a mismatch. Ripio sends the body as:
  • Compact JSON: no whitespace after separators ({"key":"value"}, not {"key": "value"}).
  • ASCII-escaped: non-ASCII characters are encoded as \uXXXX escapes (for example, Ñ is sent as Ñ).
  • Content-Type: application/json.

Example Code (Python)

Here’s an example in Python demonstrating how to verify the signature over the raw request body:
import hmac
import hashlib

def verify_signature(raw_body: bytes, secret: str, signature_header: str) -> bool:
    """
    Verify that the webhook was sent by Ripio by validating the HMAC-SHA256
    signature against the raw request body.

    Parameters:
    raw_body (bytes): The raw bytes of the webhook request body, exactly as received.
    secret (str): The shared secret key.
    signature_header (str): The value of the `Http-X-Wh-Signature-256` header.

    Returns:
    bool: True if the signature is valid, False otherwise.
    """
    expected_signature = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        msg=raw_body,
        digestmod=hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected_signature, signature_header)

Important Notes for Signature Validation

  • Verify Over Raw Bytes: Always run HMAC over the raw request body. Parsing the JSON and re-serializing it on your side will change the bytes (whitespace, escape sequences, key order) and cause verification to fail.
  • Secure Secret Storage: Always use a secure method to store and handle your shared secret key. Do not embed it directly in your client-side code or commit it to version control.
  • Reject Invalid Requests: Reject any requests with missing or invalid signatures to prevent unauthorized access or processing of tampered data.
  • Timestamp Verification (Optional but Recommended): Consider checking the issueDatetime of the webhook event to prevent replay attacks, where an attacker resends an old, valid webhook. Define an acceptable time window for webhooks.

Webhook Event Categories

Ripio provides webhooks for three main categories of events:
  • On-Ramp Events: Notifications related to the process of converting fiat currency to cryptocurrency.
  • Off-Ramp Events: Notifications related to the process of converting cryptocurrency back to fiat currency.
  • Sell and Pay Events: Notifications related to Sell and Pay transactions, which convert cryptocurrency to fiat and complete QR code payments to merchants.
  • KYC Events: Notifications related to KYC (Know Your Customer) verification status changes.
Please refer to the specific pages for detailed information on event types and payload structures for each category.

Configuring Your Webhook Endpoint

To receive webhook notifications, you need to configure a publicly accessible HTTPS URL endpoint in your Ripio partner settings. Ripio will send POST requests to this URL with a JSON payload. Key considerations for your endpoint:
  • HTTPS: Your endpoint URL must use HTTPS.
  • Respond Quickly: Your endpoint should acknowledge receipt of the webhook by returning a 2xx HTTP status code (e.g., 200 OK or 202 Accepted) as quickly as possible, ideally within 10 seconds as per Ripio’s guidelines.
  • Asynchronous Processing: For any time-consuming processing of the webhook data, perform it asynchronously (e.g., using a message queue) to ensure your endpoint responds promptly.
  • Idempotency: Design your webhook handler to be idempotent. This means that processing the same event multiple times should not result in duplicated actions or inconsistent state, as network issues or retries might cause a webhook to be delivered more than once.
For specific instructions on configuring your webhook URL within your Ripio account, please reach out to the Ripio technical team.

Basic Endpoint Implementation Examples

Here are basic examples of how you might set up an endpoint to receive webhooks and validate signatures.

Python (Flask Example)

from flask import Flask, request, abort
import hmac
import hashlib
import json

app = Flask(__name__)

# Replace with your actual shared secret provided by Ripio
SHARED_SECRET = "your_ripio_shared_secret"

def generate_expected_signature(payload_bytes: bytes, secret: str) -> str:
    hash_object = hmac.new(
        secret.encode("utf-8"),
        msg=payload_bytes,
        digestmod=hashlib.sha256
    )
    return "sha256=" + hash_object.hexdigest()

@app.route('/ripio-webhook-handler', methods=['POST'])
def ripio_webhook_handler():
    received_signature = request.headers.get('Http-X-Wh-Signature-256')
    
    # Get raw body bytes for signature verification
    raw_payload_bytes = request.get_data() # Important: use raw bytes

    if not received_signature:
        print("Error: Missing Http-X-Wh-Signature-256 header")
        abort(400, 'Missing signature header')

    if not raw_payload_bytes:
        print("Error: Missing payload")
        abort(400, 'Missing payload')

    expected_signature = generate_expected_signature(raw_payload_bytes, SHARED_SECRET)

    if not hmac.compare_digest(expected_signature, received_signature):
        print(f"Error: Invalid signature. Expected: {expected_signature}, Received: {received_signature}")
        abort(403, 'Invalid signature')

    # If signature is valid, parse the JSON payload for processing
    try:
        payload_json = json.loads(raw_payload_bytes.decode('utf-8'))
    except json.JSONDecodeError:
        print("Error: Could not decode JSON payload")
        abort(400, 'Invalid JSON payload')

    print("Webhook received and signature validated!")
    print("Event Type:", payload_json.get("eventType"))
    print("Payload:", payload_json)

    # TODO: Add your asynchronous business logic here
    # (e.g., add to a queue for processing)

    return "Webhook processed successfully", 200

if __name__ == '__main__':
    # For development only. Use a proper WSGI server in production.
    app.run(port=5000, debug=True)

Node.js (Express Example)

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

const app = express();
const port = 3000;

// Replace with your actual shared secret provided by Ripio
const SHARED_SECRET = 'your_ripio_shared_secret';

// Middleware to get raw body for signature verification
// Important: This needs to be before express.json() if you also want to use it
app.use(bodyParser.json({
  verify: (req, res, buf, encoding) => {
    if (buf && buf.length) {
      req.rawBody = buf.toString(encoding || 'utf8');
    }
  }
}));

app.post('/ripio-webhook-handler', (req, res) => {
  const receivedSignature = req.headers['http-x-wh-signature-256']; // Headers are lowercased by some servers/frameworks

  if (!receivedSignature) {
    console.error('Error: Missing Http-X-Wh-Signature-256 header');
    return res.status(400).send('Missing signature header');
  }

  if (!req.rawBody) {
    console.error('Error: Missing raw body payload');
    return res.status(400).send('Missing payload');
  }

  const hmac = crypto.createHmac('sha256', SHARED_SECRET);
  const expectedSignature = 'sha256=' + hmac.update(req.rawBody).digest('hex');

  // Use crypto.timingSafeEqual for comparing signatures to prevent timing attacks
  const signaturesMatch = crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'utf8'),
    Buffer.from(receivedSignature, 'utf8')
  );

  if (!signaturesMatch) {
    console.error(`Error: Invalid signature. Expected: ${expectedSignature}, Received: ${receivedSignature}`);
    return res.status(403).send('Invalid signature');
  }

  // Signature is valid, req.body contains the parsed JSON payload (due to bodyParser.json())
  const payload = req.body; 
  console.log('Webhook received and signature validated!');
  console.log('Event Type:', payload.eventType);
  console.log('Payload:', payload);

  // TODO: Add your asynchronous business logic here
  // (e.g., add to a message queue for processing)

  res.status(200).send('Webhook processed successfully');
});

app.listen(port, () => {
  console.log(`Webhook handler listening at http://localhost:${port}/ripio-webhook-handler`);
});
Remember to replace "your_ripio_shared_secret" with the actual secret provided by Ripio.