Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.clevis.dev/llms.txt

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 transitionPENDING, 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.

Subscribing

Pass notification_url on create. HTTPS onlyhttp:// URLs are rejected with 422 VALIDATION_ERROR.
{
  "amount": "4600000.00",
  "currency": "COP",
  "country": "CO",
  "external_id": "payroll-co-2026-05-emp-001",
  "notification_url": "https://client.example.com/webhooks/clevis-payouts",
  "beneficiary": { ... }
}
The webhook fires on every transition the payout goes through, so a happy-path payout produces three events: PENDING, PROCESSING, PAID.

Event shape

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

Signature verification

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 hmac
import hashlib
import os
from fastapi import FastAPI, Request, HTTPException

app = 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.

Delivery semantics in v1

Propertyv1 (mock)Real provider (future)
DeliveryAt-most-onceAt-least-once
RetriesNoneExponential backoff
Timeout5 seconds (configurable)5 seconds (configurable)
OrderBest effortBest effort
Mock markerX-Clevis-Mock: true always presentHeader dropped
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.

Mock-only knobs

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

Testing locally

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.