Developers / Webhooks

Webhooks

We notify your server every time an escrow transaction changes state. Every payload is signed with HMAC-SHA256 so you can verify it came from harrenapay.

Events

EventFires when
payment.successBuyer paid in full. Funds are now held in escrow.
payment.failedPayment was abandoned, declined, or underpaid.
escrow.releasedBuyer (or auto-release) released funds to the seller.
escrow.refundedFunds returned to the buyer after dispute or cancellation.
charge.successDirect (non-escrow) charge succeeded and was settled instantly.
charge.failedDirect charge was declined, abandoned, or underpaid.

Payload

JSON body. reference is your original order id; amount is in kobo.

POST your endpoint
{
  "event": "payment.success",
  "reference": "order_4821",
  "amount": 2500000,
  "currency": "NGN"
}

Signature

Header x-harrenapay-signature is the hex-encoded HMAC-SHA256 of the raw request bodyusing your endpoint signing secret (whsec_...). Always verify BEFORE parsing JSON, and compare in constant time.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyHarrenaSignature(rawBody: string, header: string | null, secret: string) {
  if (!header) return false;
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(header, "hex");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}
Python
import hmac, hashlib

def verify_harrena_signature(raw_body: bytes, header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")
Express handler
import express from "express";

app.post("/api/harrenapay-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("x-harrenapay-signature");
    const raw = req.body.toString();
    if (!verifyHarrenaSignature(raw, sig, process.env.HARRENAPAY_WEBHOOK_SECRET)) {
      return res.status(401).send("Invalid signature");
    }
    const evt = JSON.parse(raw);
    switch (evt.event) {
      case "payment.success":  /* mark order paid, release goods */ break;
      case "payment.failed":   /* cancel order */ break;
      case "escrow.released":  /* funds released to seller */ break;
      case "escrow.refunded":  /* funds returned to buyer */ break;
    }
    res.status(200).send("ok");
  }
);

Delivery and retries

  • Respond with 2xx within 10 seconds. Any other status is treated as a failure.
  • Failed deliveries retry once automatically on 5xx or network error.
  • You can manually replay any past delivery from the in-app Delivery logs tab.
  • Duplicates are possible. Use reference as your idempotency key.

Register an endpoint

Add your HTTPS callback URL and pick events from the in-app Developers tab. The signing secret is shown immediately, rotate it any time.

Open Developers tab

harrenapay is operated by Harrena Africa Synergy LTD. harrenapay is not a bank. Payment processing is provided by Paystack, our CBN-regulated licensed partner.