Overview

When a payment status changes, RohoPay sends a POST request to the callback_url you provided. This is the authoritative notification — more reliable than query parameters in redirect URLs.

Webhook Events

EventTrigger
deposit.successfulA mobile money collection or card payment was confirmed
withdraw.successfulA disbursement or wallet withdrawal succeeded
withdraw.failedA disbursement or withdrawal failed (funds not sent)

Payload

{
  "event": "deposit.successful",
  "id": "01j2k3m4n5p6q7r8s9t0uvwx",
  "internal_reference": "RHP-2024-ABC123",
  "provider_reference": "9876543210",
  "type": "collection",
  "status": "successful",
  "payment_method": "mobile_money",
  "phone_number": "256700123456",
  "amount": 50000,
  "currency": "UGX",
  "commission_amount": 500,
  "net_amount": 49500,
  "environment": "live",
  "created_at": "2024-07-15T08:30:00Z",
  "updated_at": "2024-07-15T08:31:47Z"
}

Signature Verification

RohoPay signs every outgoing webhook with HMAC-SHA256. The signature is in the x-rohopay-signature header:
x-rohopay-signature: sha256=abc123def456...
Your webhook secret is visible in Dashboard → Webhooks. Set it as an environment variable on your server:
ROHOPAY_WEBHOOK_SECRET=your-secret-from-dashboard

Verification Code

import crypto from "crypto";

export async function POST(req: Request) {
  const sig = req.headers.get("x-rohopay-signature") ?? "";
  const rawBody = await req.text();

  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.ROHOPAY_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return new Response("Unauthorized", { status: 401 });
  }

  const event = JSON.parse(rawBody);

  if (event.event === "deposit.successful") {
    // Fulfil order, credit customer account, etc.
    await creditAccount(event.internal_reference, event.net_amount);
  }

  if (event.event === "withdraw.failed") {
    // Notify customer that payout failed
    await notifyCustomer(event.internal_reference);
  }

  return new Response("OK", { status: 200 });
}

Delivery Requirements

  • Return HTTP 200–299 within 10 seconds
  • If your endpoint times out or returns a non-2xx code, RohoPay retries with exponential backoff
  • Process heavy logic asynchronously: return 200 first, then process in a background job

View Webhook Config in Dashboard

Go to Dashboard → Webhooks to:
  • Copy your webhook secret
  • See the signature header name (x-rohopay-signature)
  • Test your webhook endpoint

Testing Locally

Use a tunnel to expose your local server during development:
# ngrok
ngrok http 3000

# Cloudflare
cloudflared tunnel --url http://localhost:3000
Use the tunnel URL as your callback_url in test-mode requests.
Always verify the signature before processing the event. Never skip verification — even in development mode. An unverified webhook handler is a security vulnerability.