Skip to content

Webhooks API

Register webhook subscriptions and manage event delivery.

Base permission: IsAdminOrAbove

Endpoints

Method Path Description Permission
GET /webhooks List subscriptions IsAdminOrAbove
GET /webhooks/{id} Get subscription IsAdminOrAbove
POST /webhooks Create subscription IsAdminOrAbove
PUT /webhooks/{id} Update subscription IsAdminOrAbove
DELETE /webhooks/{id} Delete subscription IsAdminOrAbove
POST /webhooks/{id}/retry-delivery Retry failed delivery IsAdminOrAbove
GET /webhooks/{id}/dead-letters List failed deliveries IsAdminOrAbove
POST /webhooks/{id}/re-enable Re-enable disabled subscription IsAdminOrAbove

Create Subscription

POST /api/v1/webhooks
{
  "url": "https://example.com/webhooks/lms",
  "events": ["loan.created", "loan.status_changed", "payment.received"],
  "secret": "whsec_abc123...",
  "is_active": true
}

Warning

Webhook URLs must use HTTPS. HTTP URLs are rejected at creation time.

Update Subscription

PUT /api/v1/webhooks/{id}
{
  "events": ["loan.created", "loan.status_changed", "payment.received", "payment.reversed"],
  "is_active": true
}

Event Types

Event Trigger
loan.created New loan created
loan.status_changed Loan status transition
loan.disbursed Loan funded
payment.received Payment completed
payment.reversed Payment reversed
payment.failed Payment processing failed
delinquency.changed DPD bucket transition
fee.assessed Fee assessed on loan
modification.completed Modification applied
compliance.failed Compliance check failed
document.generated Document generated

Payload Format

All events follow a standard envelope:

{
  "event_type": "payment.received",
  "timestamp": "2026-01-15T14:30:00Z",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "payment_id": "123e4567-e89b-12d3-a456-426614174000",
    "loan_id": "789e0123-e89b-12d3-a456-426614174000",
    "amount": "500.00"
  }
}

Security

HMAC Verification

Every delivery includes an HMAC-SHA256 signature:

X-Webhook-Signature: sha256=<hex_digest>

Verify in your endpoint:

import hmac
import hashlib

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

Delivery & Retry

Retry Policy

Failed deliveries are retried with exponential backoff:

Attempt Delay
1st retry 10 seconds
2nd retry 60 seconds
3rd retry 300 seconds (5 minutes)

Maximum 3 retry attempts per delivery.

Dead Letters

After 5 consecutive failures, the subscription is auto-disabled:

  • is_active set to false
  • last_error records the failure reason
  • Use POST /webhooks/{id}/re-enable to reactivate after fixing the endpoint

Retry Failed Delivery

Manually retry a specific failed delivery:

POST /api/v1/webhooks/{id}/retry-delivery
{
  "delivery_id": "..."
}

List Dead Letters

View all failed deliveries for a subscription:

GET /api/v1/webhooks/{id}/dead-letters

Response:

[
  {
    "id": "...",
    "event_type": "payment.received",
    "payload": { "..." },
    "response_status": 500,
    "response_body": "Internal Server Error",
    "attempted_at": "2026-01-15T14:30:00Z",
    "retry_count": 3
  }
]

See Also