Webhooks
At-least-once notifications, each one signed, so your systems learn that something real happened in LendEasy without polling for it.
Webhooks are how your system finds out that something real happened in LendEasy without asking every few seconds whether it has. They are at-least-once notifications: duplicates, retries, and out-of-order delivery are ordinary conditions rather than incidents — and a consumer written as though each event arrives once, in order, posts the same payment twice the first time a delivery is retried.
- LendEasy signs the raw bytestimestamp, delivery ID, versioned signature
- Your endpoint verifiesbefore anything parses business data
- eventId inserted uniquelywhere a duplicate cannot slip past
- Acknowledge with a 2xxwithin five seconds, once safely queued
- Business work runs afterloan reads, provider calls, calculationswaits
The signing secret is shown exactly once
curl -X POST "$BASE/v1/servicing/webhook-subscriptions" \
-H "Authorization: Bearer $TOKEN" \
-H "LendEasy-Tenant: demo-lender" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: portfolio-webhook-v1" \
-d '{
"url": "https://events.example.test/lendeasy",
"eventTypes": [
"payment.posted",
"payment.returned",
"funding.posted",
"case.transitioned"
],
"apiVersion": "2026-07-01",
"description": "Portfolio event consumer"
}'
The create response returns signingSecret exactly once. Later reads expose only secretVersion, so a secret that was never captured is recovered by rotating it rather than by reading it back.
The payload is a snapshot, not the state
{
"eventId": "evt_PAY_81",
"eventType": "payment.posted",
"apiVersion": "2026-07-01",
"occurredAt": "2026-09-15T14:22:08Z",
"subject": { "type": "payment", "id": "9011" },
"data": {
"paymentId": 9011,
"loanRef": "7204",
"status": "POSTED",
"amount": "517.14",
"currency": "USD",
"effectiveDate": "2026-09-12"
}
}
The payload is a notification snapshot, not a substitute for an authorized API read when your workflow needs current state.
Verify before parsing business data
LendEasy signs the exact raw request bytes with the active secret. Verification order:
1. Read raw bytes without JSON reserialization.
2. Reject timestamps outside your replay window.
3. Compute HMAC over timestamp + separator + raw bytes.
4. Compare signatures in constant time against active overlap secrets.
5. Deduplicate eventId, then parse and enqueue.
Keep the signature, the secret, and the unrestricted payload out of your logs: a signature sitting next to the bytes it signs is a delivery that anybody who can read that log can replay against your endpoint. During rotation, accept the old and new secret only until previousSecretExpiresAt — a secret you keep honouring past that point is a second live key, and the rotation has not actually happened.
Acknowledge fast, then do the work
Heavy work done before the acknowledgement runs past the delivery timeout, the event is retried, and the same slow work starts again on the next copy — and the copy after that.
Non-2xx responses and timeouts retry with increasing delay. A 2xx means your endpoint accepted responsibility; it does not mean your downstream workflow finished.
The timeline is truth, not arrival order
- Put a unique constraint on
eventId— with at-least-once delivery a second copy is a matter of when, not whether. - Track the last applied resource version for state projections.
- Ignore an older version after a newer version, but retain it for diagnostics.
- Fetch current state when a missing predecessor matters.
- Make side effects idempotent independently of webhook deduplication.
payment.returned may reach one consumer before a delayed payment.posted delivery. A consumer that applies them in the order they landed records a payment that was returned before it was ever made, and every figure downstream inherits that.
Five operations run the delivery side
You can inspect and change delivery without touching the consumer. Five operations, and none destroys history:
| Operation | Purpose |
|---|---|
POST /v1/servicing/webhook-subscriptions/{id}/test |
Send a signed non-business test event through the normal path. |
GET /v1/servicing/webhook-subscriptions/{id}/deliveries |
Inspect attempts, response codes, latency, and retry state. |
POST /v1/servicing/webhook-subscriptions/{id}/rotate-secret |
Rotate with a bounded overlap window. |
PATCH /v1/servicing/webhook-subscriptions/{id} |
Change filters or destination with optimistic concurrency. |
POST /v1/servicing/webhook-subscriptions/{id}/deactivate |
Stop new deliveries while retaining history. |
Subscribe narrowly, then read current state
Published families include customer and consent lifecycle, loan and schedule changes, payment and return state, funding state, statements, servicing actions, restrictions, cases, tasks, interactions, promises, documents, merchant sales/remittances, and reconciliation exceptions. Every family you take that nothing downstream consumes is traffic your endpoint has to verify, deduplicate, and acknowledge inside five seconds anyway.
How this contract is delivered today
Signed, at-least-once delivery is the contract. The surface it travels over is named here, in one place, so nothing above has to interrupt itself to say so.