Webhooks

Webhooks: Real-time Transaction Updates

Webhooks Overview

Imagine knowing the status of your transactions the moment they happen—no refreshing, no waiting. With Hurupay’s Webhooks, you can create a dynamic notification system that keeps you in the loop on all your Collection and Payout API activities.

When the status of your API calls changes, Hurupay’s resource server immediately notifies your server via webhooks. This status change, known as an event, triggers a notification that you can handle through a POST endpoint.

A webhook URL is essentially a POST endpoint that your server listens to for updates. It must be able to parse a JSON request and respond with a status OK to acknowledge receipt.

For both On Ramp and Off Ramp transactions, it’s essential to have webhooks set up to capture the final status—whether success or failure. And here’s the best part: you can configure more than one webhook URL, tailoring the experience to your needs.

Express.js Example of a Webhook Endpoint to Handle Collection Updates
router.post("/collection/webhook/url", function(req, res) {
    // Retrieve the request's body
    const event = req.body;
    console.log(event)
    res.status(200).send('OK');
});

Partner Webhooks

Ready to set up your webhooks? Let’s dive in:

Your webhook interactions require the following headers:

headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${your-api-key}`
}

Start by creating a new webhook:

POST https://sandbox.hurupay.com/v1/webhook

Request Body

Here’s how your request body should look:

Webhook type can only be:

  • collections

  • payouts

  • kyc

{
    "url":"https://sandbox.hurupay.com/v1/webhooks/collections/collections_status_update",
    "type":"collections"
}

Request Response

Upon a successful creation, you’ll receive the following response:

{
    "success": true,
    "message": "Webhook created successfully",
    "data": {
        "partnerId": "66bbfc205a5ec9406fdd2d5a",
        "url": "https://sandbox.hurupay.com/v1/webhooks/collections/collections_status_update",
        "type": "collections",
        "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt/RD+L0L5T1rK212Yv89\nhvbES7DEwgMNvj22xlEij/7Orl6CgEQDuXisjHFqiIWe05tpR6AMWlRrggb9O+kw\nZsMrHWOp9SNcNIsR8rka5fqVSbYE8mXqtw4f4MUbB8AnRQRYIyXvlxMT1eWlXTJo\nokHsiYanA2zx3uXkDs/Ia+8B2/xKvK3UUbVa2I/b+MIWk7y02wHGFE9Zu4J98+uA\nJluS9GUEyGRtxyb+muoApGlxV9j0Qw8zhr1VPb5sjOzGKwAYfiNGNZoE59BFYM9E\nM7PfO+JWSGc6RHwNSsLxVKoj42zv6LQ2nhJ15YMuVKVD5Hb57oR6hQantzZaA3gE\n5wIDAQAB\n-----END PUBLIC KEY-----\n"
    }
}

Curious to see all your webhooks? Use this command:

GET https://sandbox.hurupay.com/v1/webhook

Request Response

Here’s the response showing your existing webhooks:

{
"success": true,
"message": "webhooks fetched successfully",
"data": [
    {
        "_id": "670508254bf89d3d77025460",
        "partnerId": "66bbfc205a5ec9406fdd2d5a",
        "url": "https://sandbox.hurupay.com/v1/webhooks/collections/collections_status_update",
        "type": "collections",
        "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt/RD+L0L5T1rK212Yv89\nhvbES7DEwgMNvj22xlEij/7Orl6CgEQDuXisjHFqiIWe05tpR6AMWlRrggb9O+kw\nZsMrHWOp9SNcNIsR8rka5fqVSbYE8mXqtw4f4MUbB8AnRQRYIyXvlxMT1eWlXTJo\nokHsiYanA2zx3uXkDs/Ia+8B2/xKvK3UUbVa2I/b+MIWk7y02wHGFE9Zu4J98+uA\nJluS9GUEyGRtxyb+muoApGlxV9j0Qw8zhr1VPb5sjOzGKwAYfiNGNZoE59BFYM9E\nM7PfO+JWSGc6RHwNSsLxVKoj42zv6LQ2nhJ15YMuVKVD5Hb57oR6hQantzZaA3gE\n5wIDAQAB\n-----END PUBLIC KEY-----\n",
        "status": "active"
    }
]

}

Looking for a specific webhook? Here’s how:

GET https://sandbox.hurupay.com/v1/webhook/{webhookId}

Request Response

Retrieve details about the specified webhook:

{
    "success": true,
    "message": "webhook fetched successfully",
    "data": {
        "_id": "670508254bf89d3d77025460",
        "partnerId": "66bbfc205a5ec9406fdd2d5a",
        "url": "https://sandbox.hurupay.com/v1/webhooks/collections/collections_status_update",
        "type": "collections",
        "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt/RD+L0L5T1rK212Yv89\nhvbES7DEwgMNvj22xlEij/7Orl6CgEQDuXisjHFqiIWe05tpR6AMWlRrggb9O+kw\nZsMrHWOp9SNcNIsR8rka5fqVSbYE8mXqtw4f4MUbB8AnRQRYIyXvlxMT1eWlXTJo\nokHsiYanA2zx3uXkDs/Ia+8B2/xKvK3UUbVa2I/b+MIWk7y02wHGFE9Zu4J98+uA\nJluS9GUEyGRtxyb+muoApGlxV9j0Qw8zhr1VPb5sjOzGKwAYfiNGNZoE59BFYM9E\nM7PfO+JWSGc6RHwNSsLxVKoj42zv6LQ2nhJ15YMuVKVD5Hb57oR6hQantzZaA3gE\n5wIDAQAB\n-----END PUBLIC KEY-----\n",
        "status": "active"
        }
}

Need to toggle a webhook’s status? Here’s how:

PUT https://sandbox.hurupay.com/v1/webhook/{webhookId}

Request Body

Set the status to “active” or “disabled”:

{
    "status":"active"
}

Request Response

Confirmation of the status change:

{
    "success": true,
    "message": "webhook fetched successfully",
    "data": {
            "_id": "670508254bf89d3d77025460",
            "partnerId": "66bbfc205a5ec9406fdd2d5a",
            "url": "https://sandbox.hurupay.com/v1/webhooks/collections/collections_status_update",
            "type": "collections",
            "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt/RD+L0L5T1rK212Yv89\nhvbES7DEwgMNvj22xlEij/7Orl6CgEQDuXisjHFqiIWe05tpR6AMWlRrggb9O+kw\nZsMrHWOp9SNcNIsR8rka5fqVSbYE8mXqtw4f4MUbB8AnRQRYIyXvlxMT1eWlXTJo\nokHsiYanA2zx3uXkDs/Ia+8B2/xKvK3UUbVa2I/b+MIWk7y02wHGFE9Zu4J98+uA\nJluS9GUEyGRtxyb+muoApGlxV9j0Qw8zhr1VPb5sjOzGKwAYfiNGNZoE59BFYM9E\nM7PfO+JWSGc6RHwNSsLxVKoj42zv6LQ2nhJ15YMuVKVD5Hb57oR6hQantzZaA3gE\n5wIDAQAB\n-----END PUBLIC KEY-----\n",
            "status": "active"
        }
}

Ready to clean up? Deleting a webhook is simple:

DELETE https://sandbox.hurupay.com/v1/webhook/{webhookId}

Request Response

You’ll receive a confirmation like this:

{
    "success": true,
    "message": "Webhook deleted successfully",
    "data": {}
}

Webhook Event Structure

We have launched webhooks for Collections, Payouts, and User KYC, and we plan to expand support to additional event categories in the coming weeks.

Your feedback is essential to us. Please feel free to suggest any priority event categories you would like us to focus on for future updates.

Below is a successful event for collections

{
    api_version: 'v1',
    event_id: '1234567890abcdef',
    event_category: 'collection',
    event_type: 'collection.successful',
    event_object: {
        type: 'collections',
        id: 'col_9876543210abcdef',
        partner: 'partner_ABC123',
        customer_name: 'John Doe',
        collection_currency: 'GHS',
        collection_rail: 'MTN',
        collection_amount: 5000,
        blockchain_network: 'CELO',
        blockchain_token: 'cUSD',
        blockchain_proof: 'https://explorer.celo.org/alfajores/tx/0xabc123def456gh7890ijklmnopqrs123456',
        token_amount: 500,
        description: 'Mobile collection for invoice #INV-2024-001',
    },
    event_created_at: '2024-10-10',
};

Event Definition

As illustrated in the event samples above, each webhook event includes the following fields:

  • api_version : Currently set to v1, which is the version in use by Hurupay APIs.

  • event_id : A globally unique identifier assigned to this event.

  • event_category :

  • collections

  • payouts

  • kyc

  • event_type : Structured as “<event_category>.<mutation_type>”.

Possible mutation types include:

  • created

  • updated

  • canceled

  • failed

  • successful

  • declined

Examples of event types:

Collections:

  • collections.created

  • collections.declined

  • collections.failed

  • collections.successful

Payouts:

  • payouts.declined

  • payouts.successful

KYC:

  • kyc.created

  • kyc.updated

  • kyc.failed

  • kyc.successful

Webhook Signature Verification

Each webhook sent will include a signature in the header with x-webhook-signature, which consists of the request body. This can be verified using the webhook’s public key.

Javascript Verification Function

import crypto from 'crypto';

function verifyWebhookSignature(body: string, signature: string, publicKey: string): boolean {
    const hash = crypto.createHash('SHA256');
    hash.update(body);
    const hashedData = hash.digest('hex');

    // Create a verifier for SHA256
    const verifier = crypto.createVerify('SHA256');
    verifier.update(hashedData);
    verifier.end();

    // Verify the signature using the public key
    return verifier.verify(publicKey, Buffer.from(signature, 'base64'));
}

Ruby Verification Function

require 'openssl'

def verify_webhook_signature(body, signature, public_key)
  hashed_data = OpenSSL::Digest::SHA256.hexdigest(body)
  verifier = OpenSSL::PKey::RSA.new(public_key)
  verifier.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), hashed_data)
end

Python Verification Function

import hashlib
import base64
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15

def verify_webhook_signature(body: str, signature: str, public_key: str) -> bool:
    hashed_data = hashlib.sha256(body.encode()).digest()
    rsa_key = RSA.import_key(public_key)

    try:
        pkcs1_15.new(rsa_key).verify(hashed_data, base64.b64decode(signature))
        return True
    except (ValueError, TypeError):
        return False

Go Verification Function

package main

import (
    "crypto/rsa"
    "crypto/sha256"
    "encoding/base64"
    "errors"
)

func verifyWebhookSignature(body string, signature string, publicKey *rsa.PublicKey) (bool, error) {
    hashedData := sha256.Sum256([]byte(body))
    decodedSignature, err := base64.StdEncoding.DecodeString(signature)
    if err != nil {
        return false, err
    }

    err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashedData[:], decodedSignature)
    if err != nil {
        return false, errors.New("signature verification failed")
    }
    return true, nil
}