Skip to Content
đź‘‹ Welcome to 100Pay Developers
DocsWebhooksCustomer Events

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

EventTrigger
customer.createdA customer was created
customer.updatedA customer’s profile was updated
customer.deletedA customer was soft-deleted
customer.restoredA deleted customer was restored
customer.suspendedYou suspended the customer
customer.resumedYou lifted a suspension
customer.restrictedYou restricted the customer
customer.unrestrictedYou lifted a restriction
customer.virtual_bank_account.pendingA virtual bank account was requested and is being provisioned
customer.virtual_bank_account.activeThe virtual bank account is active and can receive deposits
customer.virtual_bank_account.failedProvisioning failed
customer.virtual_bank_account.suspendedThe virtual bank account was suspended
customer.virtual_bank_account.closedThe virtual bank account was closed
customer.deposit.creditedA deposit into a customer’s virtual bank account was credited
customer.deposit.reversedA 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:

HeaderDescription
x-100pay-event-idThe event’s unique ID (matches id in the body)
x-100pay-event-refThe customer’s public ID
x-100pay-timestampUnix timestamp (seconds) when this delivery attempt was signed
x-100pay-signaturesha256=<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 using createdAt and the customer’s status.
  • 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-timestamp on every attempt; x-100pay-delivery-id stays stable per (event, endpoint).
Last updated on