Lumeo Docs
API Reference

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

MethodPathDescription
POST/webhooksRegister a new webhook endpoint
GET/webhooksList registered endpoints
GET/webhooks/:idGet endpoint details and recent delivery log
PATCH/webhooks/:idUpdate the URL or subscribed event list
DELETE/webhooks/:idRemove an endpoint
POST/webhooks/:id/rotateRotate the signing secret

Event catalogue

EventFires when
payout.initiatedA payout intent is recorded (INITIATED status)
payout.confirmedA payout is confirmed and enters compliance check
payout.settledA payout reaches CONFIRMED/COMPLETED
payout.failedA payout reaches FAILED status
payout.manual_reviewA payout is flagged for compliance review
fira.generatedA FIRA is auto-generated for a cross-border payment
reconciliation.completedA payment is fully reconciled against invoice + ledger
reconciliation.pendingAuto-matching failed; manual resolution needed
tax.updatedTax liability or ITR-4 prefill data changes
yield.allocatedFunds 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", 200
import (
    "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 2xx response 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 ABANDONED and no further retries occur. The event is still queryable in the delivery log.
  • Retries deliver the same event id. Key your handler on id to stay idempotent.
Lumeo
Your endpoint
payout.settled
t+0s
timeout / non-2xx
t+10s
Retry #1
+30s
Retry #2
+2min
Retry #3
+10min
Retry #4
+30min
Retry #5
+2h
Retry #6
+6h
Retry #7 (final)
+24h
ABANDONED
~33h total
A failed delivery is retried 7 times over ~33 hours before being marked ABANDONED. Every retry carries the same event id.

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.

On this page