Webhook Events & Integration

This page documents the public webhook consumption rules used by the public /api payment surface.

Overview

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

Public Contract Notes

For the default public paymentAPI router:

  • webhook consumption is part of the public integration surface

  • endpoint registration and secret rotation are account settings, not routes in the public /api contract

  • signing secrets should be stored server-side and rotated from authenticated account tooling when needed

Private Management Contract

Webhook endpoint management is intentionally outside the public /api payment contract. Piaxis dashboard and developer tooling use authenticated account routes under /users:

  • POST /users/webhooks creates a merchant webhook endpoint.

  • GET /users/merchant/webhooks lists the authenticated merchant’s endpoints.

  • PATCH /users/merchant/webhooks/{webhook_id} updates URL, description, events, activity state, or rotates the signing secret.

  • POST /users/merchant/webhooks/{webhook_id}/test sends a live test event to the configured endpoint.

  • DELETE /users/merchant/webhooks/{webhook_id} removes an endpoint.

Creation accepts a URL, description, and subscribed events. If no custom secret is supplied, Piaxis generates one; the full signing secret is returned only once, immediately after create or rotation. Store it server-side and rotate it from authenticated dashboard tooling when needed.

Merchant/developer webhook deliveries use the top-level event field instead. Do not treat this dashboard-management envelope as the same payload shape used by older public disbursement helper events.

Example test delivery envelope:

{
  "event": "payment.succeeded",
  "webhook_id": "0e4102fa-3119-42aa-8a9c-d92ef4fa83af",
  "merchant_id": "7c74f7bd-9e57-4c49-a215-8480bb81a0c0",
  "data": {
    "status": "succeeded"
  },
  "timestamp": "2026-01-15T10:30:00+00:00"
}

Delivery Pattern

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>

The public helper uses these headers when a signing secret is configured.

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 data.event in the JSON body for routing

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

  • return a 2xx response after durable handling

Public Event Types

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

  • disbursement.status_updated

  • disbursement.cancelled

Future public events will remain documented in the public payment API reference.

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("data", {}).get("event")

    if event_name == "disbursement.status_updated":
        # 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.