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, and transfer.reversed.
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, within 10 seconds: acknowledge first and do
any slow work afterwards. Failures (and no answer in time) 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.
Deduplicate on the event id
Delivery is at-least-once: the same event can reach you more than once, when
an attempt your server did receive is retried (a 2xx that was slow, or lost on
the way back) or when you redeliver it from the dashboard. Every attempt and
every redelivery of one event carries the same id (evt_…). Record the ids
you have processed, and answer a repeat with 2xx without acting on it again,
above all for charge.success, order.paid and any other event you fulfil on.
Each of your endpoints receives its own copy of an event, with its own id.
Events are not guaranteed to arrive in order: a retried event can arrive after
later ones. Read the state from the resource in data (its status), not from
the order the events came in.