Use this file to discover all available pages before exploring further.
If you supply notification_url on POST /v1/payouts, the provider POSTs a
signed event to that URL on every status transition — PENDING,
PROCESSING, PAID, REJECTED, CANCELLED. No polling required.
Sandbox mock webhooks are fire-and-forget. A non-2xx response or a timeout
is logged at WARNING and not retried. The production enviroment will add
at-least-once delivery with retries; for now, treat webhooks as a
“nice push hint” and back them with GET /v1/payouts/{id} polling for
correctness.
// POST https://client.example.com/webhooks/clevis-payouts// Headers:// Content-Type: application/json// X-Clevis-Signature: sha256=<hex HMAC-SHA256 of the raw body>// X-Clevis-Mock: true{ "type": "payout.status_changed", "id": "evt_01JABC...", "created_at": "2026-05-12T15:04:07Z", "previous_status": "PENDING", "data": { // The full Payout resource at the NEW state. "id": "po_01J...", "object": "payout", "mock": true, "status": "PROCESSING", "status_history": [ ... ], // ... every other Payout field }}
data is the full Payout resource after the transition — same shape as
GET /v1/payouts/{id}. Use data.status and previous_status to drive
your handler.
Every webhook is signed with HMAC-SHA256 over the raw response body, using
your webhook secret. The signature is sent in the X-Clevis-Signature header
as:
X-Clevis-Signature: sha256=<lowercase hex digest>
Verify the signature before trusting any field on the event. Use a
constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual
in Node.js) to avoid timing attacks.
import hmacimport hashlibimport osfrom fastapi import FastAPI, Request, HTTPExceptionapp = FastAPI()SECRET = os.environ["CLEVIS_WEBHOOK_SECRET"].encode()@app.post("/webhooks/clevis-payouts")async def handle(request: Request): raw = await request.body() sent = request.headers.get("X-Clevis-Signature", "") if not sent.startswith("sha256="): raise HTTPException(400, "Missing or malformed signature") expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(sent, expected): raise HTTPException(401, "Bad signature") event = await request.json() payout = event["data"] print( f"{payout['id']}: {event['previous_status']} -> {payout['status']}" ) return {"ok": True}
Compute the HMAC over the raw body bytes, not the parsed-and-restringified
JSON. In Node.js with Express that means express.raw(); in Python with
FastAPI, await request.body() before reading JSON.
Always back your webhook handler with GET /v1/payouts/{id} reconciliation
on a timer — in v1 because there are no retries, and in the long term to handle
out-of-order delivery and missed events from any push system.
processing.step_seconds: 0 on create — no webhooks fire, because all
transitions are applied synchronously before the create response is
returned. The terminal state is already in the response body.
processing.step_seconds: 30 (or any non-zero value) — webhooks fire on
every transition, separated by that many seconds.
A common pattern: point notification_url at an ngrok
or Cloudflare Tunnel URL
running on your machine, and use a MOCK_SLOW_external_id to make the
transitions visible at human pace.