Webhooks
Get notified the moment a transaction settles, and verify it's really us.
Polling a status endpoint works, but a webhook tells you the moment something changes — a charge succeeds, a transfer fails, a fund collection expires. Configure a webhookUrl on your application and Orivanta Pay will POST to it as events happen.
Payload shape
Every delivery has the same envelope; only event and the contents of data change:
{
"event": "payment.success",
"timestamp": 1735900000,
"applicationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"data": {
"reference": "PI-A1B2C3D4E5F6",
"type": "CHARGE",
"status": "SUCCESS",
"amount": 5000,
"currency": "XAF",
"feeAmount": 75,
"netAmount": 4925,
"paymentMethod": "MTN_MOMO_CM",
"payerAccount": "670000000",
"payerName": "Amina Ndongo",
"payerEmail": null,
"merchantReference": "CMD-2024-002",
"description": null,
"createdAt": "2026-09-05T10:00:00Z",
"updatedAt": "2026-09-05T10:02:14Z",
"failureCode": null,
"failureReason": null
}
}timestamp is Unix seconds, not milliseconds. A retried delivery gets a fresh timestamp and a fresh signature — don't treat it as the original event's creation time.
Events
| Event | Fires when |
|---|---|
payment.created | A pay-in transaction is created (checkout or charge). |
payment.success | A pay-in transaction succeeds. |
payment.failed | A pay-in transaction fails. |
payment.cancelled | A customer cancels a hosted checkout session. |
payment.expired | A hosted checkout session expires unpaid. |
payout.created | A transfer is created. |
payout.success | A transfer succeeds. |
payout.failed | A transfer fails. |
payout.cancelled | A transfer is cancelled. |
collection.expired | A fund collection link passes its expiry date. |
webhook.test | Sent by the "test webhook" action in your dashboard — not a real transaction. |
Pay-in payloads carry payer fields (payerAccount, payerName, payerEmail); pay-out payloads carry beneficiary fields (beneficiaryAccount, beneficiaryName, beneficiaryEmail) instead. Both carry feeAmount/netAmount alongside the gross amount.
Verifying the signature
Every delivery carries two headers:
X-Sharepay-Signature: t=1735900000,v1=5257a869e7bfa601d3f74b1e6c8a3f...
X-Sharepay-Event: payment.successHeader name
The header is literally X-Sharepay-Signature — that's the codename the
API is implemented under, not a typo. Match it exactly.
The signature is HMAC-SHA256 over the raw request body bytes (not a re-serialized version of the JSON — parsing then re-stringifying can reorder keys or change whitespace and silently break verification), using the webhook secret shown when you generated it (whsec_...).
import crypto from "node:crypto";
function verifyWebhook(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.split("=")),
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
if (expected !== parts.v1) {
throw new Error("Signature mismatch");
}
}
// in your route handler, using the UNPARSED body:
app.post("/webhooks/orivanta", express.raw({ type: "application/json" }), (req, res) => {
verifyWebhook(
req.body.toString("utf8"),
req.header("X-Sharepay-Signature"),
process.env.ORIVANTA_WEBHOOK_SECRET,
);
const event = JSON.parse(req.body);
// handle event.event / event.data
res.sendStatus(200);
});Unsigned deliveries
If no webhook secret is configured on the application, deliveries are still
sent — just without X-Sharepay-Signature. Treat any unsigned request to
your webhook endpoint as untrusted, and generate a secret before relying on
webhooks for anything that changes state on your side.
Retries
A delivery that doesn't get a 2xx response is retried up to 5 times, with increasing backoff:
| Attempt | Retried after |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | — gives up, delivery stays FAILED |
Because retries can arrive well after the original event, always key your handling off the transaction reference inside data, not off delivery order or timestamp. Handle repeats of the same event idempotently — the same reference reaching SUCCESS twice should be a no-op on your side, not a double-fulfillment.