Skip to main content

Webhook

Always validate webhooks first

Always verify that a webhook came from NombaSub before you process it, update a customer record, mark an invoice as paid, or grant value to a user. Your webhook endpoint is a public URL, so anyone can try to send HTTP requests to it. NombaSub signs each merchant webhook delivery with the webhook secret configured for your tenant. Validate the signature before trusting the event.
Reject webhook requests that are missing required signature headers or whose signature does not match the value you compute on your server.

Webhook headers

Every webhook delivery includes headers that identify the event, delivery, tenant, and delivery time. Header names are case insensitive, so normalize header names in your framework before reading them.
Content-Type: application/json
x-nombasub-event: invoice.paid
x-nombasub-webhook-id: 4f91b391-6d86-4394-b1f3-8ff17b18d2e4
x-nombasub-tenant-id: 7d5dbf4f-4b48-44a4-8be3-bf5a9fefaf3a
x-nombasub-timestamp: 2026-07-02T10:20:00Z
x-nombasub-signature: Bt4eJ1oL5GgdlpG8uGg2A8f8j3dWjT6Un1E2C9uF8gQ=
HeaderDescription
x-nombasub-eventThe event type, such as invoice.paid, invoice.payment_failed, or subscription.created.
x-nombasub-webhook-idUnique webhook delivery ID. Use this for idempotency.
x-nombasub-tenant-idTenant ID the event belongs to.
x-nombasub-timestampRFC3339 UTC timestamp for when the delivery attempt was sent.
x-nombasub-signatureBase64-encoded HMAC-SHA256 signature generated with your webhook secret.

How signature verification works

NombaSub creates the signature from this exact string:
eventType:webhookId:tenantId:timestamp
For example:
invoice.paid:4f91b391-6d86-4394-b1f3-8ff17b18d2e4:7d5dbf4f-4b48-44a4-8be3-bf5a9fefaf3a:2026-07-02T10:20:00Z
Then NombaSub signs that string using HMAC-SHA256 with your webhook secret and base64-encodes the result. To verify a webhook:
  1. Read x-nombasub-event, x-nombasub-webhook-id, x-nombasub-tenant-id, x-nombasub-timestamp, and x-nombasub-signature from the request headers.
  2. Build the signature payload as eventType:webhookId:tenantId:timestamp.
  3. Generate a base64 HMAC-SHA256 signature using your webhook secret.
  4. Compare your generated signature with x-nombasub-signature using a timing-safe comparison.
  5. Process the webhook only when the signatures match.
The signature is based on the webhook headers above, not on the raw JSON request body.

Signature verification examples

import base64
import hashlib
import hmac
from flask import Flask, request, abort, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = "replace_with_your_webhook_secret"


def calculate_signature(event_type, webhook_id, tenant_id, timestamp):
    signing_payload = f"{event_type}:{webhook_id}:{tenant_id}:{timestamp}"
    digest = hmac.new(
        WEBHOOK_SECRET.encode("utf-8"),
        signing_payload.encode("utf-8"),
        hashlib.sha256,
    ).digest()
    return base64.b64encode(digest).decode("utf-8")


def verify_nombasub_webhook(headers):
    event_type = headers.get("x-nombasub-event")
    webhook_id = headers.get("x-nombasub-webhook-id")
    tenant_id = headers.get("x-nombasub-tenant-id")
    timestamp = headers.get("x-nombasub-timestamp")
    received_signature = headers.get("x-nombasub-signature")

    if not all([event_type, webhook_id, tenant_id, timestamp, received_signature]):
        return False

    expected_signature = calculate_signature(event_type, webhook_id, tenant_id, timestamp)
    return hmac.compare_digest(expected_signature, received_signature)


@app.post("/webhooks/nombasub")
def handle_webhook():
    if not verify_nombasub_webhook(request.headers):
        abort(401)

    payload = request.get_json(silent=True) or {}
    # Process the trusted webhook asynchronously where possible.
    return jsonify({"received": True}), 200

Webhook payload

Webhook payloads use a common envelope:
{
  "id": "4f91b391-6d86-4394-b1f3-8ff17b18d2e4",
  "eventType": "invoice.paid",
  "tenantId": "7d5dbf4f-4b48-44a4-8be3-bf5a9fefaf3a",
  "data": {
    "code": "INV_z9y8x7w6",
    "status": "paid",
    "amountDue": 500000,
    "amountPaid": 500000,
    "currency": "NGN"
  },
  "createdAt": "2026-07-02T10:20:00Z"
}
FieldTypeDescription
idstringUnique webhook delivery ID.
eventTypestringEvent type that triggered the webhook.
tenantIdstringTenant the event belongs to.
dataobjectEvent-specific payload.
createdAtstringTimestamp for when the webhook payload was created.

Processing checklist

  • Validate the signature before processing the event.
  • Return a 2xx response quickly after accepting the webhook.
  • Use x-nombasub-webhook-id or payload id to make processing idempotent.
  • Process business logic asynchronously where possible.
  • Do not grant value, mark an invoice as paid, or change subscription state when signature validation fails.