API ReferenceWebhooks

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/endpoints
GET/v1/webhooks/endpoints
POST/v1/webhooks/:deliveryId/replay
GET/v1/webhooks/deliveries/dead

How webhooks work

When a payment event occurs, POPFAB:

  1. Constructs a signed event payload.
  2. Sends a POST request to each registered endpoint that is subscribed to the event type.
  3. Expects an HTTP 2xx response within 10 seconds.
  4. If the response is not 2xx or times out, retries with exponential backoff across 8 attempts.
  5. 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

AttemptDelay after previous failure
Attempt 1Immediate
Attempt 230 seconds
Attempt 32 minutes
Attempt 410 minutes
Attempt 530 minutes
Attempt 62 hours
Attempt 76 hours
Attempt 824 hours → dead letter

Register a Webhook Endpoint

POST
/v1/webhooks/endpoints

Registers a URL to receive webhook events.

ParameterTypeRequiredDescription
urlstringRequiredThe HTTPS URL POPFAB will POST events to. Must be publicly reachable.
eventsstring[]RequiredArray of event types to subscribe to. See event reference below.
enabledbooleanOptionalWhether 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/endpoints

Returns 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

EventDescription
payment.initiatedA payment request was received and queued for processing.
payment.successPayment confirmed by the provider. Safe to fulfill the order.
payment.failedPayment failed across all attempted providers.
payment.reversedA refund or reversal was successfully processed.
payment.pending_confirmationProvider accepted the request; awaiting final settlement (bank transfers).
webhook.undeliverableA webhook delivery exhausted all retry attempts. Action required.
provider.degradedA provider's circuit breaker has opened due to high failure rates.
provider.recoveredA degraded provider has recovered and is receiving traffic again.

Replay a Delivery

POST
/v1/webhooks/:deliveryId/replay

Queues 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/dead

Lists 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.