Lumeo Docs

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