Webhooks Reference

Receive real-time payment notifications from RohoPay and verify their signatures.

How it works

RohoPay sends you a webhook (an HTTP POST) every time a transaction you initiated changes status — e.g. a customer pays, or a payout settles. You supply a callback_url when you create the transaction; RohoPay signs the body with HMAC-SHA256 and posts it there. Verify the signature with the secret shown in your dashboard, then return any 2xx to acknowledge.

💡

RohoPay also receives internal, server-to-server status callbacks on its own payment rails. Those endpoints aren't yours to call and aren't part of the public API surface — this page covers only the webhooks RohoPay sends to you.

1. Set your callback_url

Add callback_url to the request that creates the transaction. RohoPay fires the webhook there when the status changes.

response.jsonJSON
{
  "phone":        "256712345678",
  "amount":       5000,
  "currency":     "UGX",
  "description":  "Order #1024",
  "callback_url": "https://your-app.com/webhooks/rohopay"
}

Use an HTTPS URL you control. Webhooks retry with backoff, so make the endpoint idempotent (see step 3).

2. The webhook payload

RohoPay POSTs a JSON body like this:

response.jsonJSON
{
  "event": "deposit.successful",
  "transaction_id": "01j2k3m4n5p6q7r8s9t0uvwx",
  "reference": "RHP-2024-ABC123",
  "status": "successful",
  "amount": 50000,
  "currency": "UGX",
  "type": "collection",
  "environment": "live",
  "provider": "rohopay",
  "timestamp": "2024-07-15T08:31:47Z"
}
EventTrigger
deposit.successfulMobile money or card collection confirmed (funds credited)
deposit.failedCollection rejected, expired, or timed out (no wallet change)
withdraw.successfulDisbursement / withdrawal delivered
withdraw.failedDisbursement / withdrawal failed (wallet refunded)

3. Verify the signature

Every webhook carries the signature in the X-RohoPay-Signature header:

output.txtText
X-RohoPay-Signature: sha256=abc123def456...

The signature is sha256= + HMAC-SHA256 of the raw request body, keyed with your webhook secret.

Where is my secret? Open Dashboard → Webhooks — the "Signing Secret" card shows it. Programmatically, call GET /dashboard/webhook-config (your session cookie authenticates the request). One secret covers all of your projects' outgoing webhooks.

Verify it before you trust or act on the payload:

index.tsTypeScript
import crypto from "crypto";

function verify(body: Buffer, sigHeader: string, secret: string): boolean {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  const a = Buffer.from(sigHeader);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
⚠️

Verify the raw body bytes, not a re-serialized JSON object — even a single whitespace difference breaks the HMAC.

Full receiver examples

Read the raw body (required for a correct HMAC).

Compare the header against sha256=HMAC(secret, rawBody) in constant time. Reject with 401 on mismatch.

Handle the event idempotently — key off transaction_id, since RohoPay may retry. Return any 2xx to stop retries.

index.jsJS
const express = require("express");
const crypto = require("crypto");

const app = express();

// Keep the raw body so the HMAC matches byte-for-byte.
app.post("/webhooks/rohopay",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const secret = process.env.ROHOPAY_WEBHOOK_SECRET; // from Dashboard → Webhooks
    const sig = req.get("X-RohoPay-Signature") || "";
    const expected = "sha256=" + crypto
      .createHmac("sha256", secret)
      .update(req.body) // raw Buffer
      .digest("hex");

    const a = Buffer.from(sig);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).send("Invalid signature");
    }

    const event = JSON.parse(req.body.toString());
    console.log("Webhook:", event.event, event.transaction_id, event.status);

    // Idempotent — RohoPay may retry, so guard on event.transaction_id.
    switch (event.event) {
      case "deposit.successful": /* credit your customer */ break;
      case "withdraw.failed":    /* notify / refund */      break;
    }

    res.json({ received: true }); // any 2xx stops retries
  }
);

app.listen(3000);
index.phpPHP
<?php
$payload = file_get_contents("php://input");          // raw body
$sig     = $_SERVER["HTTP_X_ROHOPAY_SIGNATURE"] ?? "";
$secret  = getenv("ROHOPAY_WEBHOOK_SECRET");          // from Dashboard → Webhooks

$expected = "sha256=" . hash_hmac("sha256", $payload, $secret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit("Invalid signature");
}

$event = json_decode($payload, true);
// Idempotent — guard on $event["transaction_id"] (RohoPay may retry).
switch ($event["event"]) {
    case "deposit.successful": /* credit your customer */ break;
    case "withdraw.failed":    /* notify / refund */      break;
}

http_response_code(200);
echo json_encode(["received" => true]);

Delivery behaviour

  • Trigger: status change only (pendingsuccessful / failed)
  • Method: POST with Content-Type: application/json
  • Retries: up to 3 attempts with 1s, 4s, 9s backoff
  • Timeout: 10 seconds per attempt
  • Acknowledge: return any 2xx to stop retries; anything else is retried
  • Idempotency: webhooks may repeat — dedupe on transaction_id

Get webhook config (API)

terminal.shBash
GET /dashboard/webhook-config
Cookie: session={your_session}
response.jsonJSON
{
  "success": true,
  "data": {
    "webhook_secret": "your-secret",
    "signature_header": "X-RohoPay-Signature",
    "signature_format": "sha256={hex_digest}",
    "events": [
      { "event": "deposit.successful", "description": "Collection confirmed" },
      { "event": "deposit.failed", "description": "Collection failed" },
      { "event": "withdraw.successful", "description": "Payout sent" },
      { "event": "withdraw.failed", "description": "Payout failed" }
    ]
  }
}