Webhook Events & Integration

This page documents both the public webhook consumption rules used by the public /api payment surface and the deployment-specific merchant/developer webhook flow used by private dashboard tooling.

Overview

Webhook delivery lets your system react to payment, escrow, disbursement, and merchant event changes without aggressive polling.

Contract Split

There are two webhook surfaces in the current codebase:

  • the default public /api payment surface, which emits outbound webhook deliveries but does not expose public webhook endpoint management routes

  • a deployment-specific merchant/developer webhook flow, typically managed from private dashboard tooling rather than the default public /api router

Public Contract Notes

For the default public paymentAPI router:

  • webhook consumption is part of the public integration surface

  • webhook endpoint creation is not currently exposed as a default public /api route

If your deployment exposes dashboard or private tooling for webhook endpoint registration, treat that as deployment-specific configuration rather than part of the guaranteed public /api contract.

Some deployments also expose private developer tooling to create endpoints, set or rotate a signing secret, and trigger a manual test delivery. In those flows, the full signing secret should be treated as write-only and copied when it is created or rotated.

Private Merchant/Developer Management

When the private merchant/developer webhook flow is enabled, the current code supports routes such as:

  • POST /users/webhooks to create an endpoint

  • GET /users/merchant/webhooks to list configured endpoints

  • PATCH /users/merchant/webhooks/{webhook_id} to update URL, description, subscribed events, active state, or signing secret

  • POST /users/merchant/webhooks/{webhook_id}/test to send a real test delivery using the first subscribed event

  • DELETE /users/merchant/webhooks/{webhook_id} to remove an endpoint

Secret Lifecycle

For the merchant/developer webhook flow:

  • create accepts url, optional description, one or more events, and an optional custom secret

  • if secret is omitted, Piaxis generates one server-side

  • the full signing secret is returned only once, immediately after create or secret rotation

  • later list/profile responses expose only metadata such as has_secret and secret_hint

Merchant/Developer Delivery Pattern

The merchant/developer webhook model currently posts JSON with this envelope:

{
  "event": "payment.succeeded",
  "data": {
    "test": true,
    "merchant_id": "6c15f7e8-335a-4c62-a993-fb06a27b7787",
    "merchant_name": "Demo Merchant",
    "payment_id": "3a0da2c0-a8b2-4c76-9f40-1dbf7ee03dcf",
    "reference": "test_9f7d1bd7b5aa",
    "currency": "UGX",
    "amount": 150000,
    "status": "succeeded"
  },
  "timestamp": "2026-04-14T10:30:00.000000",
  "webhook_id": "0e4102fa-3119-42aa-8a9c-d92ef4fa83af"
}

The manual test-delivery endpoint uses the first subscribed event on the webhook and marks the event payload with "test": true.

Public Payment Helper Delivery Pattern

The shared helper used by the public payment API posts JSON with this envelope:

{
  "data": {
    "event": "disbursement.status_updated",
    "disbursement_id": "b6b93a8b-2c81-44bf-9d98-9a43f725b90f",
    "status": "completed"
  },
  "timestamp": "2026-01-15T10:30:00+00:00"
}

Headers

 Content-Type: application/json
 User-Agent: piaxis-Webhook/1.0
X-piaxis-Signature: <hex_hmac_sha256_if_secret_is_configured>
X-piaxis-Event: <event_name>

Both the merchant/developer flow and the public payment helper currently use these headers.

Merchant/Developer Event Catalog

The current merchant/developer webhook event set is:

  • order.created

  • order.updated

  • order.completed

  • payment.succeeded

  • payment.failed

  • refund.processed

  • escrow.released

  • dispute.opened

  • dispute.closed

Signature Verification

When a webhook secret is configured, Piaxis computes:

  • HMAC SHA256

  • over the raw JSON payload body

  • using the shared webhook secret

Python example

import hashlib
import hmac


def verify_signature(raw_body: bytes, header_value: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_value)

Consumer Requirements

Your webhook endpoint should:

  • accept HTTP POST requests

  • read the raw body before mutating it

  • verify X-piaxis-Signature when a secret is configured

  • use X-piaxis-Event or the event field in the JSON body for routing

  • treat delivery as at-least-once and process idempotently

  • return a 2xx response after durable handling

Public Helper Event Types

The public payment surface may deliver event names inside data.event. Event names vary by subsystem. Examples observed in the current public surface include:

  • disbursement.status_updated

  • disbursement.cancelled

Merchant/developer webhook deliveries use the top-level event field instead. If your deployment consumes both surfaces, route by payload.get("event") first and fall back to payload.get("data", {}).get("event").

Minimal Handler Example

from fastapi import FastAPI, Header, HTTPException, Request

app = FastAPI()
WEBHOOK_SECRET = "your_webhook_secret"


@app.post("/webhooks/piaxis")
async def handle_piaxis_webhook(
    request: Request,
    x_piaxis_signature: str | None = Header(default=None),
):
    raw_body = await request.body()

    if x_piaxis_signature:
        if not verify_signature(raw_body, x_piaxis_signature, WEBHOOK_SECRET):
            raise HTTPException(status_code=401, detail="Invalid webhook signature")

      payload = await request.json()
      event_name = payload.get("event") or payload.get("data", {}).get("event")

      if event_name == "payment.succeeded":
        # update your internal payment tracking here
        pass

    return {"ok": True}

Operational Guidance

  • Store webhook deliveries with your own request id or event id when present.

  • Make webhook handlers idempotent.

  • Reconcile with polling when a webhook is missed.

  • Never assume a richer payload than the current event requires.
    • If your deployment exposes private dashboard tooling, use the returned delivery metadata such as last event, HTTP status, error text, and response preview for debugging failed endpoints.