Webhooks
100Pay sends webhook events to your server in real time when something happens in your account — a payment is received, a transfer completes, a deposit comes in, a transaction changes status, or a customer record is updated. Use webhooks to trigger order fulfillment, update records, or notify users.
Your webhook endpoint must be publicly accessible over HTTPS (not localhost). Register it in your Developer Settings .
Setup
In your Developer Settings , configure one or more webhook endpoints. Each endpoint has:
| Field | Value |
|---|---|
| Webhook URL | Your server endpoint, e.g. https://yourapp.com/api/webhooks/100pay |
| Verification Token | A per-endpoint secret used to verify that events are genuinely from 100Pay |
You can register multiple endpoints — every event is delivered independently to each active endpoint. Duplicate URLs are automatically de-duplicated.
Event Catalog
| Event | Trigger | Details |
|---|---|---|
Payment charge (no eventType — see below) | A payment on a charge is confirmed (checkout, payment link, or API charge) | Payment events |
transaction.deposit | A wallet credit transaction was recorded | Transaction events |
transaction.withdrawal | A wallet debit transaction was recorded | Transaction events |
transaction.status.changed | A transaction moved between statuses (e.g. pending → successful) | Transaction events |
reward.payout | A reward was paid to a wallet | Transaction events |
bank_transfer.debit | An outgoing bank transfer changed status | Bank transfer events |
bank_transfer.credit | An incoming bank transfer was received | Bank transfer events |
wallet.deposit | An on-chain crypto deposit arrived at a wallet address | Wallet deposit events |
wallet.deposit.internal | An internal transfer between 100Pay wallets was received | Wallet deposit events |
transaction_session.completed / transaction_session.expired | A payment session finished or timed out | Session callbacks |
customer.* (15 events) | Customer lifecycle, virtual bank account, and deposit events | Customer events |
Delivery Headers
Every webhook request is an HTTP POST with Content-Type: application/json and the following headers:
| Header | Description |
|---|---|
verification-token | The endpoint’s verification token — compare against your stored secret |
x-100pay-event-type | The event type (e.g. transaction.deposit). unknown for legacy payment-charge payloads |
x-100pay-event-ref | A stable reference for the underlying resource (transaction ID, charge ID, session ID, …) |
x-100pay-delivery-id | Unique ID for this logical delivery — stable across retries. Use it for idempotency |
idempotency-key | Same value as x-100pay-delivery-id |
x-100pay-webhook-id | The ID of the endpoint the event was delivered to |
x-100pay-attempt-number | 1 for the initial delivery, incremented on each retry |
Customer events additionally include x-100pay-event-id, x-100pay-timestamp, and an HMAC signature header x-100pay-signature.
Verifying Webhooks
Always verify the verification-token header before processing an event:
// Express.js example
app.post("/api/webhooks/100pay", express.json(), (req, res) => {
const token = req.headers["verification-token"];
if (token !== process.env.PAY100_VERIFICATION_TOKEN) {
return res.status(401).json({ error: "Unauthorized" });
}
// Acknowledge fast — process asynchronously
res.status(200).json({ received: true });
handleWebhookEvent(req.body).catch(console.error);
});Never skip verification. Without it, anyone could send fake events to your endpoint. For customer.* events, verify the HMAC signature as well — see Customer events.
For defense in depth, verify critical state server-side (e.g. call the payment verification API) before releasing goods or crediting balances.
Routing Events
Most payloads carry an eventType field. The legacy payment-charge payload does not — identify it by the presence of chargeId:
async function handleWebhookEvent(event: Record<string, any>) {
switch (event.eventType ?? event.event) {
case "transaction.deposit":
case "transaction.withdrawal":
case "reward.payout":
return handleTransaction(event);
case "transaction.status.changed":
return handleStatusChange(event);
case "bank_transfer.debit":
case "bank_transfer.credit":
return handleBankTransfer(event);
case "wallet.deposit":
case "wallet.deposit.internal":
return handleWalletDeposit(event);
case "transaction_session.completed":
case "transaction_session.expired":
return handleSession(event);
default:
if (event.eventType?.startsWith("customer.") || event.type?.startsWith("customer."))
return handleCustomerEvent(event);
// Legacy payment charge payload — no eventType field
if (event.chargeId && event.type === "credit")
return handlePaymentCharge(event);
console.log("Unhandled event:", event.eventType ?? event.type);
}
}Idempotency & Duplicate Suppression
100Pay reserves a delivery record before any outbound request, so the same logical event is sent at most once per endpoint — but network conditions mean your endpoint may still occasionally see duplicates (e.g. your 200 response was lost and the delivery was retried).
- Store
x-100pay-delivery-idand skip events you’ve already processed. It is stable across retries of the same delivery. - As a fallback,
x-100pay-event-ref(or the payload’stransactionId/chargeId) identifies the underlying resource. - Some events legitimately fire once per state transition — e.g.
bank_transfer.debitfires again when a transfer moves fromCreatedtoSuccessful, andtransaction.status.changedfires per transition. Each transition has a distinct delivery ID.
Retries
If your endpoint returns a non-2xx response or times out (15 seconds), delivery is retried with back-off:
| Attempt | Delay after previous failure |
|---|---|
| 1 | immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
After 4 failed attempts the delivery is marked exhausted and will not be retried. customer.* events use their own retry loop (exponential back-off, 30s–1h) with a 72-hour delivery deadline.
Best Practices
- Respond
2xxwithin 15 seconds — acknowledge first, then process asynchronously (queue, background job). - Be idempotent — key your processing on
x-100pay-delivery-id. - Verify the
verification-tokenheader (and HMAC for customer events) on every request. - Log raw payloads for debugging and audit trails.
- Don’t rely on ordering — events can arrive out of order, especially across retries. Use
timestamp/previousStatusfields to reconcile. - Use HTTPS with a valid certificate.