Webhooks
POPFAB sends signed HTTP POST requests to your endpoints when payment events occur. Delivery is guaranteed with automatic retries and dead-letter support.
Endpoints
POST
/v1/webhooks/endpointsGET
/v1/webhooks/endpointsPOST
/v1/webhooks/:deliveryId/replayGET
/v1/webhooks/deliveries/deadHow webhooks work
When a payment event occurs, POPFAB:
- Constructs a signed event payload.
- Sends a
POSTrequest to each registered endpoint that is subscribed to the event type. - Expects an HTTP
2xxresponse within 10 seconds. - If the response is not 2xx or times out, retries with exponential backoff across 8 attempts.
- After all retries fail, the delivery moves to the dead-letter queue where you can replay it.
✓
Respond with
200 OK as quickly as possible. Offload heavy processing to a background queue — do not block your webhook handler on database writes or external calls.Retry schedule
| Attempt | Delay after previous failure |
|---|---|
| Attempt 1 | Immediate |
| Attempt 2 | 30 seconds |
| Attempt 3 | 2 minutes |
| Attempt 4 | 10 minutes |
| Attempt 5 | 30 minutes |
| Attempt 6 | 2 hours |
| Attempt 7 | 6 hours |
| Attempt 8 | 24 hours → dead letter |
Register a Webhook Endpoint
POST
/v1/webhooks/endpointsRegisters a URL to receive webhook events.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Required | The HTTPS URL POPFAB will POST events to. Must be publicly reachable. |
events | string[] | Required | Array of event types to subscribe to. See event reference below. |
enabled | boolean | Optional | Whether the endpoint is active. Defaults to true. |
Register an endpointbash
curl -X POST https://api.popfab.io/v1/webhooks/endpoints \
-H "Authorization: Bearer sk_test_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/popfab",
"events": [
"payment.success",
"payment.failed",
"payment.reversed"
],
"enabled": true
}'Responsejson
{
"id": "ppfb_ep_01HX9T2KBQ",
"url": "https://yourapp.com/webhooks/popfab",
"events": ["payment.success", "payment.failed", "payment.reversed"],
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"enabled": true,
"created_at": "2025-03-19T10:00:00.000Z"
}✕
The
secret is only returned once at endpoint creation. Store it immediately — you will need it to verify incoming webhook signatures.List Webhook Endpoints
GET
/v1/webhooks/endpointsReturns all registered webhook endpoints. The secret is masked after creation.
List endpointsbash
curl https://api.popfab.io/v1/webhooks/endpoints \
-H "Authorization: Bearer sk_test_YOUR_API_KEY"Verifying Signatures
Every webhook request includes an X-POPFAB-Signature header. This is an HMAC-SHA256 digest of the raw request body, signed with your endpoint secret. Always verify this signature before processing the event.
Signature verification — Node.jsjavascript
import crypto from 'crypto';
export async function POST(request) {
const rawBody = await request.text();
const signature = request.headers.get('X-POPFAB-Signature');
const secret = process.env.POPFAB_WEBHOOK_SECRET;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature ?? ''),
Buffer.from(expected)
)) {
return new Response('Forbidden', { status: 403 });
}
const event = JSON.parse(rawBody);
// Process event...
return new Response('OK', { status: 200 });
}Signature verification — Pythonpython
import hmac
import hashlib
import os
def verify_signature(body: bytes, signature: str) -> bool:
secret = os.environ["POPFAB_WEBHOOK_SECRET"].encode()
expected = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# In your Flask/FastAPI handler:
@app.post("/webhooks/popfab")
async def handle_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-POPFAB-Signature", "")
if not verify_signature(body, signature):
raise HTTPException(status_code=403)
event = await request.json()
# Process event...
return {"status": "ok"}Event Payload Structure
Example payment.success eventjson
{
"id": "evt_01HX9T2KBQ",
"event": "payment.success",
"created_at": "2025-03-19T10:25:00.000Z",
"data": {
"id": "ppfb_pay_01HX9T2KBQM4Z3YWN5E6R7VP8S",
"reference": "MYAPP-ORDER-7890",
"amount": 150000,
"currency": "NGN",
"payment_method": "card",
"status": "success",
"provider": "paystack",
"provider_reference": "psk_T8x29kLMqpn",
"customer": {
"email": "ada@example.com",
"name": "Ada Okafor"
},
"metadata": { "order_id": "7890" },
"fees": {
"popfab_fee": 225,
"provider_fee": 375,
"total_fee": 600
},
"created_at": "2025-03-19T10:23:45.000Z",
"updated_at": "2025-03-19T10:25:00.000Z"
}
}Event Types
| Event | Description |
|---|---|
payment.initiated | A payment request was received and queued for processing. |
payment.success | Payment confirmed by the provider. Safe to fulfill the order. |
payment.failed | Payment failed across all attempted providers. |
payment.reversed | A refund or reversal was successfully processed. |
payment.pending_confirmation | Provider accepted the request; awaiting final settlement (bank transfers). |
webhook.undeliverable | A webhook delivery exhausted all retry attempts. Action required. |
provider.degraded | A provider's circuit breaker has opened due to high failure rates. |
provider.recovered | A degraded provider has recovered and is receiving traffic again. |
Replay a Delivery
POST
/v1/webhooks/:deliveryId/replayQueues a new delivery attempt for a previously failed or dead-lettered event.
Replay a deliverybash
curl -X POST https://api.popfab.io/v1/webhooks/dlv_01HX9T2KBQ/replay \
-H "Authorization: Bearer sk_test_YOUR_API_KEY"Dead-Letter Queue
GET
/v1/webhooks/deliveries/deadLists all deliveries that exhausted all retry attempts and could not be delivered.
List dead-letter deliveriesbash
curl https://api.popfab.io/v1/webhooks/deliveries/dead \
-H "Authorization: Bearer sk_test_YOUR_API_KEY"⚠
Events in the dead-letter queue represent data your server never confirmed receiving. Review and replay them to ensure your system is fully in sync with POPFAB.