Webhooks
Push-based event delivery for merchant trust lifecycle events.
Overview
Evodira webhooks deliver real-time HTTP POST payloads to your registered endpoint whenever a trust event occurs — no polling required. All payloads are signed with HMAC-SHA256 and delivered with retry logic and a dead-letter queue.
Webhooks are configured per platform API key via POST /platform/webhooks. You can subscribe to specific event types or omit the events field to receive all events.
Registering a Webhook
curl -X POST https://api.evodira.com/api/v1/platform/webhooks \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/evodira",
"secret": "a-strong-random-secret",
"events": ["risk_status_changed", "corrective_action_issued"],
"description": "Production webhook — risk and corrective action events"
}'The secret field is optional but strongly recommended. When present, every delivery will include an X-LMT-Signature header you can use to authenticate the payload.
Delivery Headers
| Header | Value | Description |
|---|---|---|
| X-LMT-Event | e.g. risk_status_changed | The event type that triggered this delivery |
| X-LMT-Webhook-ID | UUID | Unique ID of the webhook delivery attempt |
| X-LMT-Timestamp | ISO-8601 UTC | Time the delivery was dispatched |
| X-LMT-Signature | sha256=<hex> | HMAC-SHA256 of the raw request body. Only present if secret is configured. |
| Content-Type | application/json | Always JSON |
Payload Structure
Every webhook payload follows the same envelope:
{
"event_type": "risk_status_changed",
"event_id": "3f8b7c1a-9e2d-4a55-b6f0-112233445566",
"idempotency_key": "3f8b7c1a-9e2d-4a55-b6f0-112233445566",
"payload_version": "1",
"timestamp": "2025-01-15T10:43:22.001Z",
"data": {
"merchant_id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
"merchant_name": "Mama Cass Kitchen",
"previous_status": "verified",
"new_status": "review",
"previous_risk_level": "low",
"new_risk_level": "medium",
"reason_codes": ["complaint_spike", "evidence_quality_drop"],
"calculated_at": "2025-01-15T10:43:20.832Z"
}
}| Field | Type | Description |
|---|---|---|
| event_type | string | The event type (see Event Reference below) |
| event_id | UUID | Unique identifier for this event. Use for idempotency. |
| idempotency_key | UUID | Same as event_id — use to deduplicate replays |
| payload_version | string | Payload schema version. Currently "1". |
| timestamp | ISO-8601 UTC | When the event occurred |
| data | object | Event-specific payload fields (see each event type) |
Event Reference
Evodira emits 5 event types. Subscribe to specific events or omit the events array to receive all.
risk_status_changedA merchant's public trust status or risk level has changed (e.g. verified → suspended, or low risk → high risk).
review_case_openedA new manual review case has been opened for a merchant, either automatically by a trigger policy or manually by an operator.
review_decision_madeA reviewer has made a decision on a review case (and, if applicable, a second reviewer has approved it).
corrective_action_issuedA corrective action has been issued to a merchant, requiring them to submit remediation evidence.
trigger_firedA risk trigger has fired for a merchant, indicating a threshold or policy condition has been met.
Signature Verification
When you register a webhook with a secret, every delivery includes an X-LMT-Signature header with the value sha256=<HMAC-SHA256-hex>. Compute the HMAC over the raw request body bytes and compare using a constant-time function to prevent timing attacks.
import hmac
import hashlib
def verify_evodira_signature(
payload_bytes: bytes,
secret: str,
signature_header: str, # value of X-LMT-Signature header
) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(),
payload_bytes,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header)
# In your webhook handler:
# raw_body = await request.body()
# signature = request.headers.get("X-LMT-Signature", "")
# if not verify_evodira_signature(raw_body, WEBHOOK_SECRET, signature):
# return Response(status_code=403)Your endpoint must respond with HTTP 2xx within 30 seconds. Any other status code (including redirects) is treated as a failure.
Retry Policy & Dead Letters
Failed deliveries (non-2xx response or timeout) are retried with exponential backoff:
| Attempt | Delay after failure |
|---|---|
| 1 (initial) | Immediate |
| 2 | 60 seconds (base delay × 2⁰) |
| 3 | 120 seconds (base delay × 2¹) |
After 3 attempts (configurable via WEBHOOK_MAX_ATTEMPTS), the delivery is moved to the dead-letter queue. Dead-letter deliveries are visible via GET /platform/webhook-deliveries?dead_letter_only=true and can be manually replayed via POST /platform/webhook-deliveries/{attempt_id}/replay.
You can also trigger a bulk retry of all due deliveries via POST /platform/webhooks/retries/run-due.
Idempotency
Each delivery carries a stable idempotency_key equal to the event_id. On retry, the event_id is the same — only attempt_number changes. Store processed event_id values to safely handle duplicate deliveries.
Testing Webhooks
Use POST /platform/webhooks/test to send a synthetic event to any registered endpoint without triggering a real state change:
curl -X POST https://api.evodira.com/api/v1/platform/webhooks/test \
-H "X-API-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"event_type": "risk_status_changed",
"merchant_id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
"data": { "new_status": "suspended" }
}'See Also
Platform Integration API → — webhook registration, delivery history, and replay endpoints.