Developers

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

HeaderValueDescription
X-LMT-Evente.g. risk_status_changedThe event type that triggered this delivery
X-LMT-Webhook-IDUUIDUnique ID of the webhook delivery attempt
X-LMT-TimestampISO-8601 UTCTime the delivery was dispatched
X-LMT-Signaturesha256=<hex>HMAC-SHA256 of the raw request body. Only present if secret is configured.
Content-Typeapplication/jsonAlways 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"
  }
}
FieldTypeDescription
event_typestringThe event type (see Event Reference below)
event_idUUIDUnique identifier for this event. Use for idempotency.
idempotency_keyUUIDSame as event_id — use to deduplicate replays
payload_versionstringPayload schema version. Currently "1".
timestampISO-8601 UTCWhen the event occurred
dataobjectEvent-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_changed

A merchant's public trust status or risk level has changed (e.g. verified → suspended, or low risk → high risk).

Triggered by: Risk score recalculation produces a different public_status or risk_level
data fields:
merchant_idprevious_statusnew_statusprevious_risk_levelnew_risk_levelreason_codes[]calculated_at
review_case_opened

A new manual review case has been opened for a merchant, either automatically by a trigger policy or manually by an operator.

Triggered by: Trigger policy with creates_review_case: true fires, or operator calls POST /review-cases
data fields:
case_idmerchant_idreasonprioritycreated_atcreated_by
review_decision_made

A reviewer has made a decision on a review case (and, if applicable, a second reviewer has approved it).

Triggered by: POST /review-cases/{case_id}/decision (and optionally POST /{case_id}/second-review)
data fields:
case_idmerchant_iddecisiondecision_reasonreviewer_actordecided_atrequires_second_review
corrective_action_issued

A corrective action has been issued to a merchant, requiring them to submit remediation evidence.

Triggered by: POST /corrective-actions or trigger policy with creates_corrective_action: true
data fields:
action_idmerchant_idaction_typeprioritydue_dateissued_atissued_by
trigger_fired

A risk trigger has fired for a merchant, indicating a threshold or policy condition has been met.

Triggered by: POST /risk/merchants/{id}/evaluate-triggers detects a new trigger condition
data fields:
trigger_idmerchant_idtrigger_typeseverityreasonmetadatapolicy_versionevent_group_keycreated_at

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:

AttemptDelay after failure
1 (initial)Immediate
260 seconds (base delay × 2⁰)
3120 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.

Full API Reference →