Webhooks Rolling out
Orbita can POST to your server when something happens on your account, so you don't have to poll. Each delivery is signed so you can check it came from Orbita.
Set up an endpoint
- In the Orbita dashboard, go to Settings → Webhooks and enter an HTTPS URL for each event you want. Each event type has its own URL and its own signing secret.
- The signing secret (
whsec_…) is shown once, when you save the URL. Store it securely — you need it to verify deliveries. - The URL must be public HTTPS. Orbita doesn't follow redirects.
Events
What a delivery looks like
A POST with a JSON body — the event's payload itself, with no envelope — and these headers:
virtual_account_funding.application/jsonVerify the signature
Compute the HMAC-SHA256 of the raw body bytes with the secret for the event in X-Orbita-Event, hex-encode it and compare it to X-Orbita-Signature in constant time. Reject the delivery if they differ. Verify before parsing — re-serialising parsed JSON won't reproduce the same bytes.
// Express. Read the raw body -- verify it before parsing JSON.
const crypto = require("crypto");
const express = require("express");
// One secret per event type, from Settings -> Webhooks.
const SECRETS = {
virtual_account_funding: process.env.ORBITA_FUNDING_WEBHOOK_SECRET, // "whsec_..."
sub_wallet_provisioned: process.env.ORBITA_SUB_WALLET_WEBHOOK_SECRET,
};
const app = express();
app.post("/orbita/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const secret = SECRETS[req.get("X-Orbita-Event")];
if (!secret) return res.sendStatus(400);
const expected = crypto.createHmac("sha256", secret).update(req.body).digest("hex");
const received = req.get("X-Orbita-Signature") || "";
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const payload = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200); // respond fast, then process
// Deliveries can repeat: skip payloads you've already handled.
});Responding and retries
- Return any
2xxstatus within 10 seconds. Anything else — another status, a timeout, a connection error — counts as a failed attempt. - Failed deliveries are retried about every 30 seconds, up to 6 attempts in total. After that, Orbita stops trying.
- Because of retries, the same event can arrive more than once. Make your handler idempotent.
- To have a payment's funding webhook sent again later, use Resend a payment webhook.
virtual_account_funding
Sent once a payment into a sub-wallet has been credited to your NGN balance. You can also fetch the payment with List payments received — its providerReference equals reference below.
customerReference, or null.ngn.name, accountNumber and bankName of the account that paid. Any of them can be null when the bank doesn't provide it.null."8525000").{
"subWalletId": "c2d8e4f6-1a3b-4c5d-8e9f-0a1b2c3d4e5f",
"customerReference": "CUST-1042",
"accountNumber": "9012345678",
"amount": 150000,
"currency": "ngn",
"payer": {
"name": "CHUKWUDI OKAFOR",
"accountNumber": "0701234567",
"bankName": "Access Bank"
},
"reference": "000013260923160248000123456789",
"sessionId": "000013260923160248000123456789",
"resultingWalletBalance": "8525000",
"paidAt": "2026-09-23T15:02:48.920Z"
}sub_wallet_provisioned
Sent when Create a sub-wallet succeeds. The same details are in that request's response; this event is useful if the response was lost.
label you sent.customerReference you sent, or null.ngn.active. See Statuses.{
"subWalletId": "c2d8e4f6-1a3b-4c5d-8e9f-0a1b2c3d4e5f",
"customerLabel": "Ada Stores",
"customerReference": "CUST-1042",
"accountNumber": "9012345678",
"accountName": "Ada Stores",
"bankName": "Wema Bank",
"currency": "ngn",
"status": "active"
}