Webhooks

StarPay POSTs a signed JSON payload to your endpoint URLs when events occur. Register endpoints (each gets its own signing secret, shown once) and subscribe to the events you care about.

Payload

{
  "id": "evt_123",
  "event": "charge.success",
  "data": { "id": "ch_abc", "amount": 250000, "currency": "GHS", "status": "success" }
}

Events include charge.success, charge.failed, transfer.success, transfer.failed, transfer.reversed, deposit.confirmed, payout.success, payout.failed, and trade.settled (Phase 2).

Verify the signature

Each request carries X-Starpay-Timestamp and X-Starpay-Signature = HMAC_SHA256(endpoint_secret, "{timestamp}.{sha256(raw_body)}"). Compute the hash over the raw body, before parsing, and reject timestamps older than 300 seconds.

Python

import hashlib, hmac, time

def verify(secret, timestamp, raw_body: bytes, signature, tolerance=300) -> bool:
    if abs(time.time() - int(timestamp)) > tolerance:
        return False
    body_hash = hashlib.sha256(raw_body).hexdigest()
    expected = hmac.new(secret.encode(), f"{timestamp}.{body_hash}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Node.js

const crypto = require("crypto");

function verify(secret, timestamp, rawBody, signature, tolerance = 300) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > tolerance) return false;
  const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
  const expected = crypto.createHmac("sha256", secret)
    .update(`${timestamp}.${bodyHash}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

PHP

function starpay_verify($secret, $timestamp, $rawBody, $signature, $tolerance = 300): bool {
    if (abs(time() - (int) $timestamp) > $tolerance) return false;
    $bodyHash = hash('sha256', $rawBody);
    $expected = hash_hmac('sha256', "{$timestamp}.{$bodyHash}", $secret);
    return hash_equals($expected, $signature);
}

Retries

Respond 2xx to acknowledge. Failures are retried up to 5 times with backoff — 1m, 5m, 30m, 2h, 12h — then dead-lettered and surfaced in your dashboard, where you can inspect and redeliver them.