Webhooks
100Pay sends webhook events to your server in real time when something happens in your account — a payment is received, a transfer completes, or a wallet deposit comes in. Use webhooks to trigger order fulfillment, update records, or notify users.
Your webhook endpoint must be publicly accessible (not localhost). Register it in your Developer Settings .
Setup
In your Developer Settings , configure:
| Field | Value |
|---|---|
| Webhook URL | Your server endpoint, e.g. https://yourapp.com/api/webhooks/100pay |
| Webhook Secret Key | A secret string used to verify that events are genuinely from 100Pay |
Verifying Webhook Signatures
Every webhook request includes a x-webhook-secret (or similar) header. Always verify this on your server before processing the event.
// Express.js example
app.post("/api/webhooks/100pay", (req, res) => {
const incomingSecret = req.headers["x-webhook-secret"];
if (incomingSecret !== process.env.WEBHOOK_SECRET) {
return res.status(401).json({ error: "Unauthorized" });
}
const event = req.body;
handleWebhookEvent(event);
res.status(200).json({ received: true });
});Never skip signature verification. Without it, anyone could send fake events to your endpoint.
Event Types
| Event | Trigger |
|---|---|
bank_transfer.debit | Outgoing bank transfer was initiated from your account |
bank_transfer.credit | Incoming bank transfer was received into your account |
wallet.deposit | Crypto deposit received to a wallet address |
wallet.deposit.internal | Internal transfer received between 100Pay wallets |
Payload Schemas
bank_transfer.debit — Outgoing Transfer
{
"eventType": "bank_transfer.debit",
"transactionId": "txn_abc123",
"reference": "PAY_1709500000000",
"amount": 50000,
"timestamp": "2026-03-03T22:00:00Z",
"transaction": {
"status": "Successful",
"from": "your_account",
"to": "1234567890",
"metadata": {
"paymentReference": "PAY_1709500000000",
"description": "Payment for services",
"fees": 100
}
},
"data": {
"sessionId": "sess_abc123",
"creditAccountName": "JOHN DOE",
"creditAccountNumber": "1234567890",
"status": "Successful",
"narration": "Payment for services",
"fees": 100
}
}bank_transfer.credit — Incoming Transfer
{
"eventType": "bank_transfer.credit",
"transactionId": "txn_xyz789",
"amount": 25000,
"timestamp": "2026-03-03T22:05:00Z",
"data": {
"sessionId": "sess_xyz789",
"debitAccountName": "JANE SMITH",
"creditAccountNumber": "0987654321",
"status": "Successful",
"narration": "Transfer from Jane",
"fees": 0
}
}wallet.deposit — Crypto Wallet Deposit
{
"eventType": "wallet.deposit",
"transactionId": "txn_crypto_001",
"amount": "50.00",
"symbol": "USDT",
"network": "bsc",
"timestamp": "2026-03-03T22:10:00Z",
"data": {
"address": "0xABC...DEF",
"transactionHash": "0xhash...",
"confirmations": 12,
"status": "completed"
}
}wallet.deposit.internal — Internal Wallet Deposit
{
"eventType": "wallet.deposit.internal",
"transactionId": "txn_internal_001",
"amount": "100.00",
"symbol": "USDT",
"timestamp": "2026-03-03T22:15:00Z",
"data": {
"from": "sender@payid",
"to": "recipient@payid",
"note": "Invoice payment",
"status": "completed"
}
}Handling Events
async function handleWebhookEvent(event: { eventType: string; [key: string]: unknown }) {
switch (event.eventType) {
case "bank_transfer.debit":
// Outgoing transfer confirmed
await updateTransferRecord(event.transactionId, "completed");
break;
case "bank_transfer.credit":
// Incoming payment received
await creditUserAccount(event.amount);
break;
case "wallet.deposit":
// Crypto deposit confirmed
await handleCryptoDeposit(event);
break;
case "wallet.deposit.internal":
// Internal PayID transfer received
await handleInternalDeposit(event);
break;
default:
console.log("Unhandled event type:", event.eventType);
}
}Transfer Status Lifecycle
For bank transfers, status progresses through the following states:
| Status | Description |
|---|---|
"Created" | Transfer queued, not yet submitted to bank |
"Processing" | Submitted to the bank, awaiting confirmation |
"Successful" | Funds delivered to recipient |
"Failed" | Transfer failed — check data.failureReason |
Best Practices
- Respond with
200quickly — process events asynchronously if needed. If your server takes too long, 100Pay may retry. - Handle duplicate events — webhooks may be delivered more than once. Use the
transactionIdto deduplicate. - Log all incoming events — keep raw payloads for debugging and audit trails.
- Use HTTPS — webhook endpoints must use HTTPS to ensure payload integrity.
Retry Policy
If your endpoint returns a non-2xx response or times out, 100Pay will retry the webhook. Implement idempotent handlers using transactionId to safely handle retried events.
Next Steps
- Payment Verification — verify a payment transaction server-side
- Bank Transfers — initiate transfers and track their status
- Asset Transfers — move crypto between wallets