Webhooks
Event types, payload shapes, delivery guarantees, and signature verification.
Lumeo delivers events to your server via HTTPS POST. Subscribe to specific event types when you register a webhook endpoint. All payloads are signed with HMAC-SHA256 — always verify the signature before processing.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /webhooks | Register a new webhook endpoint |
GET | /webhooks | List registered endpoints |
GET | /webhooks/:id | Get endpoint details and recent delivery log |
PATCH | /webhooks/:id | Update the URL or subscribed event list |
DELETE | /webhooks/:id | Remove an endpoint |
POST | /webhooks/:id/rotate | Rotate the signing secret |
Event catalogue
| Event | Fires when |
|---|---|
payout.initiated | A payout intent is recorded (INITIATED status) |
payout.confirmed | A payout is confirmed and enters compliance check |
payout.settled | A payout reaches CONFIRMED/COMPLETED |
payout.failed | A payout reaches FAILED status |
payout.manual_review | A payout is flagged for compliance review |
fira.generated | A FIRA is auto-generated for a cross-border payment |
reconciliation.completed | A payment is fully reconciled against invoice + ledger |
reconciliation.pending | Auto-matching failed; manual resolution needed |
tax.updated | Tax liability or ITR-4 prefill data changes |
yield.allocated | Funds are allocated to a yield instrument |
Payload envelope
Every event shares the same outer envelope:
{
"id": "evt_01j4abc",
"type": "payout.settled",
"apiVersion": "2026-07",
"createdAt": "2026-07-01T09:14:28.000Z",
"data": {
"paymentId": "clx1a2b3c4d5e6f7g8h9",
"status": "CONFIRMED",
"amount": 500.00,
"currency": "USD",
"firaId": "fira_01j4abc"
}
}The data shape varies by event type. The id field is a stable event identifier — use it to deduplicate retries.
Signature verification
Every delivery includes an X-Lumeo-Signature header. It is an HMAC-SHA256 hex digest of the raw request body, signed with the endpoint's signing secret.
import crypto from "crypto";
export function verifyLumeoSignature(
rawBody: string,
signature: string,
secret: string,
): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
// Next.js App Router
export async function POST(req: Request) {
const raw = await req.text();
const sig = req.headers.get("x-lumeo-signature") ?? "";
if (!verifyLumeoSignature(raw, sig, process.env.LUMEO_WEBHOOK_SECRET!)) {
return new Response("Forbidden", { status: 403 });
}
const event = JSON.parse(raw);
// handle event.type ...
return new Response("ok");
}import hashlib
import hmac
def verify_lumeo_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
# Flask example
from flask import Flask, request, abort
import json
app = Flask(__name__)
@app.route("/webhooks/lumeo", methods=["POST"])
def lumeo_webhook():
raw = request.get_data()
sig = request.headers.get("X-Lumeo-Signature", "")
if not verify_lumeo_signature(raw, sig, os.environ["LUMEO_WEBHOOK_SECRET"]):
abort(403)
event = json.loads(raw)
# handle event["type"] ...
return "ok", 200import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
)
func verifyLumeoSignature(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}Always read the raw request body before parsing as JSON. Signature verification fails if the body has been transformed in any way (reformatted, key-sorted, etc.).
Delivery and retries
- Lumeo expects a
2xxresponse within 10 seconds. Anything else (non-2xx, timeout) is treated as a failed delivery. - Failed deliveries are retried with exponential backoff: after 30 s, 2 min, 10 min, 30 min, 2 h, 6 h, and 24 h — up to 7 attempts total over ~33 hours.
- After 7 failed attempts the delivery is marked
ABANDONEDand no further retries occur. The event is still queryable in the delivery log. - Retries deliver the same event
id. Key your handler onidto stay idempotent.
If your endpoint recovers partway through this sequence — say, after retry #3 — the very next attempt succeeds and no further retries fire. The full 7-attempt sequence above is the worst case, not the typical case.
See Handle webhook retries for the recommended handler pattern.