Customer Events
Fired as your customers are created, updated, and transacted with. Customer events use a modern, versioned envelope and are HMAC-signed — unlike other webhook families which rely on the verification-token header alone.
Event Types
| Event | Trigger |
|---|---|
customer.created | A customer was created |
customer.updated | A customer’s profile was updated |
customer.deleted | A customer was soft-deleted |
customer.restored | A deleted customer was restored |
customer.suspended | You suspended the customer |
customer.resumed | You lifted a suspension |
customer.restricted | You restricted the customer |
customer.unrestricted | You lifted a restriction |
customer.virtual_bank_account.pending | A virtual bank account was requested and is being provisioned |
customer.virtual_bank_account.active | The virtual bank account is active and can receive deposits |
customer.virtual_bank_account.failed | Provisioning failed |
customer.virtual_bank_account.suspended | The virtual bank account was suspended |
customer.virtual_bank_account.closed | The virtual bank account was closed |
customer.deposit.credited | A deposit into a customer’s virtual bank account was credited |
customer.deposit.reversed | A previously credited customer deposit was reversed |
Envelope
Every customer event shares this envelope:
interface CustomerEvent<T = Record<string, unknown>> {
id: string; // event ID, e.g. "webhookEvent_ab12cd34ef56"
type: string; // one of the event types above
createdAt: string; // ISO 8601
data: {
customer: {
id: string; // public customer ID, e.g. "cust_ab12cd34"
externalReference: string | null; // your reference
status: "active" | "suspended" | "restricted" | "deleted";
profileCompleteness: number; // 0–1
};
} & T; // event-specific fields below
}Lifecycle events (suspended / resumed / restricted / unrestricted)
data additionally contains:
{
"previousStatus": "active",
"effectiveStatus": "suspended",
"platformControlRemains": false
}Virtual bank account events
data.virtualBankAccount:
{
"virtualBankAccount": {
"id": "vba_ab12cd34",
"customerId": "cust_ab12cd34",
"walletId": "wal_ab12cd34",
"identityVerificationId": "idv_ab12cd34",
"externalReference": "your-ref-001",
"currency": "NGN",
"status": "active",
"autoSweep": false
}
}Deposit events
customer.deposit.credited — data.deposit:
{
"deposit": {
"transactionHash": "100000000000000000000000000002",
"amount": "50000",
"providerFee": "50",
"feeBearer": "merchant",
"currency": "NGN",
"virtualBankAccountId": "vba_ab12cd34",
"walletId": "wal_ab12cd34",
"narration": "Transfer from JANE SMITH",
"sender": { "name": "JANE SMITH", "bank": "058" }
}
}customer.deposit.reversed — data.deposit:
{
"deposit": {
"transactionHash": "100000000000000000000000000002",
"reversalTransactionHash": "100000000000000000000000000003",
"amount": "50000",
"currency": "NGN",
"virtualBankAccountId": "vba_ab12cd34",
"walletId": "wal_ab12cd34"
}
}Full example — customer.virtual_bank_account.active
{
"id": "webhookEvent_9f8e7d6c5b4a",
"type": "customer.virtual_bank_account.active",
"createdAt": "2026-03-12T16:00:00.000Z",
"data": {
"customer": {
"id": "cust_ab12cd34",
"externalReference": "user-42",
"status": "active",
"profileCompleteness": 0.85
},
"virtualBankAccount": {
"id": "vba_ab12cd34",
"customerId": "cust_ab12cd34",
"walletId": "wal_ab12cd34",
"identityVerificationId": "idv_ab12cd34",
"externalReference": "user-42-vba",
"currency": "NGN",
"status": "active",
"autoSweep": false
}
}
}Delivery Headers
In addition to the standard headers, customer events include:
| Header | Description |
|---|---|
x-100pay-event-id | The event’s unique ID (matches id in the body) |
x-100pay-event-ref | The customer’s public ID |
x-100pay-timestamp | Unix timestamp (seconds) when this delivery attempt was signed |
x-100pay-signature | sha256=<hex> — HMAC signature (see below) |
Verifying the Signature
The signature is an HMAC-SHA256 over "<timestamp>.<rawBody>" using your endpoint’s verification token as the key:
import crypto from "node:crypto";
function verifyCustomerEvent(req: {
headers: Record<string, string>;
rawBody: string; // the exact raw request body — do not re-serialize
}): boolean {
const timestamp = req.headers["x-100pay-timestamp"];
const signature = req.headers["x-100pay-signature"];
if (!timestamp || !signature) return false;
// Reject stale timestamps to prevent replay (5-minute window)
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected =
"sha256=" +
crypto
.createHmac("sha256", process.env.PAY100_VERIFICATION_TOKEN!)
.update(`${timestamp}.${req.rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}Compute the HMAC over the raw request body bytes. Parsing and re-serializing JSON can change key order or whitespace and produce a different signature. In Express, capture the raw body with express.json({ verify: (req, _res, buf) => (req.rawBody = buf.toString()) }).
Ordering & Retries
- Events carry an internal per-customer version, and duplicate
(customer, version, type)events are never created — but deliveries may still arrive out of order across retries. Reconcile usingcreatedAtand the customer’sstatus. - Customer events retry with exponential back-off (30 seconds up to 1 hour between attempts) for up to 72 hours, after which the delivery is dead-lettered.
- The signature is recomputed with a fresh
x-100pay-timestampon every attempt;x-100pay-delivery-idstays stable per (event, endpoint).