Skip to main content

Webhooks & Events

Payscribe sends HTTP callbacks to your endpoint when events occur. Webhooks are configured in the dashboard at Settings > Webhooks.

Endpoint requirements

  • Must accept POST requests
  • Must return 200 OK within 5 seconds
  • Must be publicly accessible (HTTPS)
  • Respond quickly — process events asynchronously

Signature verification

Each webhook includes these headers:

  • X-Payscribe-Signaturev1={hmac_sha256(timestamp + "." + event_id + "." + raw_body, secret)}
  • X-Payscribe-Event-Id — UUID for deduplication
  • X-Payscribe-Timestamp — Unix timestamp (reject if older than 5 minutes)

The signed value is timestamp + "." + event_id + "." + raw_body. cURL is not shown because it cannot receive an incoming webhook; choose the server language your application uses.

import crypto from 'node:crypto';
import express from 'express';

const app = express();

// Register before express.json(); req.body must remain a Buffer.
app.post('/webhooks/payscribe', express.raw({type: 'application/json'}), async (req, res) => {
const payload = req.body;
const signature = req.headers['x-payscribe-signature'];
const timestamp = req.headers['x-payscribe-timestamp'];
const eventId = req.headers['x-payscribe-event-id'];
if (!signature || !timestamp || !eventId) return res.sendStatus(401);

const expected = 'v1=' + crypto
.createHmac('sha256', process.env.PAYSCRIBE_WEBHOOK_SECRET)
.update(`${timestamp}.${eventId}.${payload.toString('utf8')}`)
.digest('hex');
const received = Buffer.from(signature);
const calculated = Buffer.from(expected);
if (received.length !== calculated.length || !crypto.timingSafeEqual(calculated, received)) {
return res.sendStatus(401);
}

// Store eventId with a unique database constraint, queue work, then acknowledge.
return res.sendStatus(200);
});

Webhook IP addresses

Whitelist these IPs in your firewall to receive webhooks from Payscribe:

18.133.55.102

Retry policy

Payscribe retries failed deliveries using exponential backoff:

AttemptDelay before retry
1st retry30 seconds
2nd retry60 seconds
3rd retry120 seconds
4th retry240 seconds
5th retry480 seconds (8 minutes)

Maximum retries: 6 (configurable in Settings > Webhooks > Advanced). After exhausting all retries, the webhook is marked as dead.

Stuck webhooks: If a webhook stays in sending state for more than 5 minutes, it is automatically reset and re-queued.

Alerting: If more than 5 webhooks fail within an hour (configurable threshold), Payscribe sends an alert email to the address configured in webhook settings.

Delivery-safe processing

Webhooks can be retried. Build consumers to be duplicate-safe and do not depend on delivery order across event types. Persist X-Payscribe-Event-Id with a unique constraint before performing fulfilment, ledger updates, or customer notifications. A duplicate insert should be treated as a successful no-op and still return 200 OK.

Return 200 OK only after the event is durably accepted for processing. Queue slow work, then use API and dashboard records to reconcile the final transaction state.

Decide what to do with an event

Use the event name and its verified payload to select an action. Do not treat every event as a fulfilment signal.

Event familyTreat it asSafe consumer action
accounts.payment.status, payment_link.paid, invoice.paidA collection outcomeReconcile the payment against your internal order or invoice, then fulfil once when the final state is confirmed.
payouts.createdA payout lifecycle updatePersist the reference and provider state. Reconcile before telling a recipient that a payout is final.
bills.created, bills.status, bills.updatedA bill fulfilment updateMatch the event to the original ref; display a token, PIN, or success only after the relevant final outcome.
cards.auth.*, cards.adjusted.*A card authorization or adjustmentRecord the event and reconcile card/transaction state. An authorization and a settlement are distinct lifecycle stages.
savings.plans.*A savings-plan lifecycle updateUpdate the plan state and reconcile any related contribution or withdrawal before showing a completed balance.
stablecoin.deposit.settledA confirmed deposit settlementMatch the address, asset, network, and transaction data before crediting the corresponding wallet state once.

For an unknown event type, persist it safely, alert or log for review, and avoid fulfilment until the event is documented. New event types can be added without changing an existing integration.

Sample payload

{
"event_id": "evt_001",
"event_type": "payouts.created",
"reference": "ref_abc123",
"amount": 10000,
"status": "processing",
"bank_code": "058",
"account_number": "0123456789"
}

Complete event catalog

Collections

EventDescription
accounts.payment.statusInbound payment detected, credited to virtual account
accounts.simulationSimulate transfer executed on the live test path (the sandbox simulate-transfer endpoint returns without dispatching)

accounts.payment.status payload

{
"event_id": "evt_002",
"event_type": "accounts.payment.status",
"trans_id": "txn_a1b2c3d4",
"amount": 5000,
"fee": 32.5,
"currency": "NGN",
"transaction": {
"session_id": "sess_xyz",
"date": "2026-07-29 10:30:00",
"bank_name": "GTBank",
"bank_code": "058",
"sender_account": "0123456789",
"sender_name": "John Doe",
"amount": 5000,
"currency": "NGN",
"description": "Payment for invoice INV-001"
},
"customer": {
"id": "cus_abc123",
"name": "John Doe",
"number": "1234567890",
"bank": "9psb",
"account_id": "va_001",
"account_type": "static"
},
"transaction_hash": "ABC123DEF...",
"created_at": "2026-07-29 10:30:00"
}

Transfers

EventDescription
payouts.createdTransfer initiated and pending processing

payouts.created payload (single transfer)

{
"event_id": "evt_003",
"event_type": "payouts.created",
"ref": "ref_001",
"trans_id": "txn_pay_001",
"session_id": "sess_abc",
"amount": 50000,
"fee": 250,
"total": 50250,
"beneficiary": {
"bank": "058",
"bank_name": "GTBank",
"account_number": "0123456789",
"account_name": "Jane Doe"
},
"currency": "NGN",
"narration": "Payment for services",
"status": "success",
"created_at": "2026-07-29 10:30:00"
}

payouts.created payload (batch)

{
"event_id": "evt_004",
"event_type": "payouts.created",
"batch_id": "batch_001",
"status": "processing",
"gross": 500000,
"count": 10,
"created_at": "2026-07-29 10:30:00"
}

Bills

EventDescription
bills.createdBill payment request created
bills.statusBill payment status updated
bills.updatedBill payment updated (e.g., delivery receipt)
bills.batch.statusBatch bill payment status update

bills.status payload

{
"event_id": "evt_005",
"event_type": "bills.status",
"trans_id": "txn_bill_001",
"transaction_status": "success",
"remark": "MTN VTU PIN: 12345678901234567890",
"service": "Airtime",
"product": "MTN",
"ref_id": "ref_001",
"amount": 500,
"total_charge": 522.25,
"discount": 22.25,
"created_at": "2026-07-29 10:30:00",
"updated_at": "2026-07-29 10:30:05"
}

Cards

EventDescription
cards.auth.activationContactless card activated at POS
cards.auth.approvedCard authorization approved
cards.auth.settledCard authorization settled (captured)
cards.auth.declinedCard authorization declined
cards.auth.verifiedCard verification ($1 auth) approved
cards.adjusted.refundCard transaction refunded
cards.adjusted.crossborderCross-border transaction settled
cards.adjusted.terminatedCard terminated
cards.adjusted.{status}Card status changed (frozen / unfrozen)

cards.auth.approved payload

{
"event_id": "evt_006",
"event_type": "cards.auth.approved",
"trans_id": "txn_card_auth_001",
"customer": {
"id": 42
},
"auth_currency": "USD",
"auth_country": "US",
"auth_amount": 49.99,
"mcc": "5812",
"acceptor_name": "AMAZON.COM",
"card": {
"id": "card_abc123",
"first_six": "411111",
"last_four": "1111",
"prev_balance": 500,
"balance": 450.01
},
"created_at": "2026-07-29 10:30:00"
}

cards.auth.declined payload

{
"event_id": "evt_007",
"event_type": "cards.auth.declined",
"trans_id": "txn_card_dec_001",
"customer": {
"id": 42
},
"acceptor_name": "NETFLIX.COM",
"auth_currency": "USD",
"auth_amount": 15.99,
"auth_country": "US",
"reason": "No sufficient funds",
"card": {
"id": "card_abc123",
"first_six": "411111",
"last_four": "1111",
"prev_balance": 10,
"balance": 10
},
"created_at": "2026-07-29 10:30:00"
}

cards.adjusted.refund payload

{
"event_id": "evt_008",
"event_type": "cards.adjusted.refund",
"trans_id": "txn_ref_001",
"customer": {
"id": 42
},
"amount": 49.99,
"acceptor_name": "AMAZON.COM",
"auth_currency": "USD",
"auth_country": "US",
"reason": "Refund",
"card": {
"id": "card_abc123",
"first_six": "411111",
"last_four": "1111",
"prev_balance": 450.01,
"balance": 500
},
"created_at": "2026-07-29 10:30:00"
}
EventDescription
payment_link.paidPayment link has been paid

payment_link.paid payload

{
"event_id": "evt_009",
"event_type": "payment_link.paid",
"link_id": "link_abc123",
"trans_id": "txn_pl_001",
"amount": 25000,
"currency": "NGN",
"customer": {
"name": "John Doe",
"email": "john@example.com",
"phone": "08012345678"
},
"created_at": "2026-07-29 10:30:00"
}

Invoices

EventDescription
invoice.sentInvoice emailed to customer
invoice.paidInvoice paid in full
invoice.partially_paidPartial payment received
invoice.overdueInvoice past due date

invoice.paid payload

{
"event_id": "evt_010",
"event_type": "invoice.paid",
"invoice_id": "inv_abc123",
"trans_id": "txn_inv_001",
"amount_paid": 50000,
"total_paid": 50000,
"total_amount": 50000,
"currency": "NGN",
"customer": {
"name": "John Doe",
"email": "john@example.com"
},
"created_at": "2026-07-29 10:30:00"
}

Savings

EventDescription
savings.plans.createdSavings plan created
savings.plans.pausedSavings plan paused / resumed / cancelled

savings.plans.created payload

{
"event_id": "evt_011",
"event_type": "savings.plans.created",
"id": "plan_abc123",
"saving_id": 42,
"title": "Vacation Fund",
"type": "target",
"currency": "NGN",
"frequency": "weekly",
"status": "active",
"next_run_at": "2026-08-05 10:00:00",
"customer": {
"id": "cus_abc123",
"name": "John Doe",
"email": "john@example.com"
}
}

savings.plans.paused payload

{
"event_id": "evt_012",
"event_type": "savings.plans.paused",
"id": "plan_abc123",
"status": "paused",
"updated_at": "2026-07-29T10:30:00Z"
}

Issuing

EventDescription
issuing.created.successfulCard created successfully (including stablecoin-funded orders, once the deposit is confirmed)

issuing.created.successful payload

{
"event_id": "evt_013",
"event_type": "issuing.created.successful",
"trans_id": "txn_iss_001",
"ref": "ref_001",
"card": {
"id": "card_abc123",
"card_type": "virtual",
"currency": "USD",
"brand": "VISA",
"name": "John Doe",
"first_six": "411111",
"last_four": "1111",
"masked": "411111 **** **** 1111",
"expiry": "06/28",
"billing": {
"street": "123 Main St",
"city": "Lagos",
"country": "NG"
},
"created_at": "2026-07-29 10:30:00"
},
"customer": {
"id": "cus_abc123",
"name": "John Doe"
}
}

Stablecoin

EventDescription
stablecoin.deposit.settledStablecoin deposit settled and credited
stablecoin.card.createdCard funded via stablecoin and created
stablecoin.card.topped_upExisting card topped up via stablecoin

stablecoin.deposit.settled payload

{
"event_id": "evt_014",
"event_type": "stablecoin.deposit.settled",
"address": "0xabc123...",
"txid": "0xblockchain_tx_hash",
"trans_id": "txn_stable_001",
"asset": "USDT",
"network": "BEP20",
"chain": "BSC",
"amount": 1000,
"amount_cleared": 997.5,
"rate": 1500,
"usd_wallet_settlement": false,
"off_ramp_fee": 2.5,
"fee": 14.96,
"amount_credited": 1473810,
"customer_id": 42,
"settled_at": "2026-07-29 10:30:00",
"created_at": "2026-07-29 10:30:00"
}

Common mistakes

MistakeFix
Re-serializing the payload before verificationUse the exact raw body: file_get_contents('php://input') (PHP) or req.body with express.raw() (Express)
Using API key instead of webhook secretUse the secret from Settings > Webhooks, not your API key
Not returning 200 within 5 secondsAcknowledge immediately, process asynchronously
Not checking timestamp stalenessReject events older than 5 minutes to prevent replay attacks

Best practices

  1. Acknowledge quickly — Return 200 OK immediately, process asynchronously
  2. Idempotency — Check for duplicate X-Payscribe-Event-Id values; webhooks may be delivered more than once
  3. Log everything — Log the raw payload, signature, and verification result for debugging
  4. Monitor failures — Check webhook logs in the dashboard for failed deliveries
  5. Reject stale events — Check X-Payscribe-Timestamp and reject events older than 5 minutes

Was this page helpful?

Report a docs issue →