How should I handle webhooks?
Use webhooks to reconcile payment, checkout, refund, subscription, and payout events from your server.
Webhooks tell your backend when something important happened after a customer or provider action. Use them for reconciliation, fulfillment, emails, subscriptions, and ledger updates.
What are webhooks for?
Use webhooks to:
- Confirm a payment completed before fulfilling an order.
- Learn when a payment failed, expired, or was cancelled.
- Track subscription creation, renewal, and cancellation events.
- Reconcile refunds, payouts, and transaction state changes.
- Recover from browser redirects that customers never complete.
Minimum safe handler
Your webhook endpoint should:
- Read the raw request body.
- Verify the signature with your webhook secret.
- Store the event ID or delivery ID.
- Ignore duplicate events safely.
- Fetch related resources from the API when you need authoritative state.
- Return a fast
2xxresponse after the event is accepted.
Common mistakes
| Mistake | Safer behavior |
|---|---|
| Trusting only the success redirect | Use webhooks or server-side API reads for fulfillment |
| Parsing JSON before signature verification | Verify against the raw request body |
| Updating orders without idempotency | Store processed event IDs |
| Assuming event order is perfect | Fetch final resource state when needed |
| Doing slow work inline | Queue long-running jobs after accepting the webhook |
What should I test?
Test successful, failed, duplicate, and delayed events in sandbox. Confirm your dashboard state, order state, and customer communication all agree.
Webhook security
To ensure the security and integrity of webhook events, lomi. signs each request sent to your endpoint using a unique secret associated with that webhook.
- Signature Header: Each webhook request includes an
`X-Lomi-Signature`HTTP header. - Webhook Secret: When you create a webhook endpoint via the API, a
`secret`(prefixed with`whsec_`) is returned. Store this secret securely, as it will not be shown again. - Verification: Your endpoint should verify the signature by computing an
`HMAC-SHA256`hash of the raw request body using the webhook secret and comparing it to the signature provided in the header.
Outbound delivery: Requests from lomi. to your URL are not authenticated with `Authorization` / `X-API-Key`, only signature verification applies. Putting the signing secret in an `Authorization` header on your expectation side will not help; middleware that requires Bearer or API-key auth on every POST will typically return `401` (details).
Refer to the Webhook Signature Verification guide for detailed implementation examples in various languages.
Webhook events
When creating or updating a webhook, you subscribe it to specific event types. lomi. will only send notifications for the events you've subscribed to.
| Event Enum | Description | Data Payload Type |
|---|---|---|
`PAYMENT_CREATED` | A new payment attempt has been initiated. | `Transaction` |
`PAYMENT_SUCCEEDED` | A one-time payment is successful. | `Transaction` |
`PAYMENT_FAILED` | A one-time payment attempt failed. | `Transaction` |
`SUBSCRIPTION_CREATED` | A new subscription is created. | `Subscription` |
`SUBSCRIPTION_UPDATED` | A subscription changed (pause, resume, plan change, cancel scheduled, etc.). | `Subscription` (with previous_attributes) |
`SUBSCRIPTION_RENEWED` | A subscription successfully renews. | `Subscription` |
`SUBSCRIPTION_CANCELLED` | A subscription is cancelled or expired. | `Subscription` |
`REFUND_COMPLETED` | A refund is successfully processed. | `Refund` |
`REFUND_FAILED` | A refund attempt failed. | `Refund` |
`REFUND_CREATED` | A refund attempt was created. | `Refund` |
`DISPUTE_CREATED` | A card dispute was opened. | `Dispute` |
`DISPUTE_UPDATED` | Dispute status or evidence changed. | `Dispute` |
`DISPUTE_CLOSED` | A dispute reached a terminal state. | `Dispute` |
`test.webhook` | A test event generated via the API. | `TestPayload` |
Renewal payment failures emit PAYMENT_FAILED on the failed transaction. There is no separate subscription payment-failed webhook. Use SUBSCRIPTION_RENEWED for successful billing cycles. See Subscriptions and Checkout behavior: Subscription checkout.
SUBSCRIPTION_UPDATED payload
Non-terminal subscription changes use a structured event envelope: current state in object, what changed in previous_attributes, and optional context for debugging.
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"event": "SUBSCRIPTION_UPDATED",
"timestamp": "2026-06-11T14:30:00.000Z",
"data": {
"object": {
"subscription_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "paused",
"price_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"cancel_at_period_end": false,
"next_billing_date": "2026-07-01",
"plan_name": "Pro Monthly",
"billing_interval": "month"
},
"previous_attributes": {
"status": "active"
},
"context": {
"source": "merchant_api",
"actor": "merchant"
}
}
}Reconcile entitlements from data.object. Use previous_attributes to branch logic (e.g. plan upgrade vs pause). context.source is merchant_api, customer_portal, or cron.
(Other event payloads are documented under their API resources, e.g. Transactions, Subscriptions.)
Webhook subscriptions and JSON payloads use SCREAMING_SNAKE_CASE event names (for example PAYMENT_SUCCEEDED). Some docs or OpenAPI tooling may show dotted names (for example payment.succeeded) as a human-readable alias for the same logical event, use the enum values above when configuring endpoints and when comparing event in request bodies.
For operations guidance (retries, idempotency, delivery logs), see Webhook reliability.
Testing webhooks
You can test your webhook endpoints using the API or `curl`.
Create a webhook for testing
First, create a webhook endpoint pointing to a test receiver (e.g., using a service like Webhook.site or a local tunnel like ngrok).
# Replace API_KEY and URL
curl -X POST "https://api.lomi.africa/webhooks" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"url": "YOUR_TEST_WEBHOOK_URL",
"authorized_events": ["PAYMENT_SUCCEEDED", "test.webhook"],
"description": "Test Endpoint"
}'Note the `id` and `secret` from the response.
Send a test event
Use the `POST /webhooks/{id}/test` endpoint to send a predefined `test.webhook` event to your endpoint.
# Replace WEBHOOK_ID and API_KEY
curl -X POST "https://api.lomi.africa/webhooks/YOUR_WEBHOOK_ID/test" \
-H "X-API-Key: YOUR_API_KEY"Check your test receiver to confirm the event was received and verify the signature using the stored secret.
Delivery logs and retries
lomi. logs webhook delivery attempts and provides endpoints to view these logs and manually retry failed deliveries.
Get webhook delivery logs
Retrieves delivery logs for a specific webhook, showing success/failure status, timestamps, response codes, etc.
Endpoint: `GET /webhooks/{id}/logs`
Path parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
`id` | `string` | Yes | ID of the webhook (`wh_...`) |
Query parameters (optional):
| Parameter | Type | Description |
|---|---|---|
`limit` | `integer` | Number of logs to return (max 100) |
`offset` | `integer` | Number of logs to skip (pagination) |
`success` | `boolean` | Filter by successful deliveries |
`failed` | `boolean` | Filter by failed deliveries |
Example request:
# Replace WEBHOOK_ID and API_KEY
curl -X GET "https://api.lomi.africa/webhooks/YOUR_WEBHOOK_ID/logs?limit=10&failed=true" \
-H "X-API-Key: YOUR_API_KEY"Retry webhook delivery
Manually triggers a retry for a specific failed delivery log entry.
Endpoint: `POST /webhooks/{webhook_id}/logs/{log_id}/retry`
Path parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
`webhook_id` | `string` | Yes | ID of the webhook (`wh_...`) |
`log_id` | `string` | Yes | ID of the failed delivery log entry |
Example request:
# Replace WEBHOOK_ID, LOG_ID and API_KEY
curl -X POST "https://api.lomi.africa/webhooks/YOUR_WEBHOOK_ID/logs/YOUR_LOG_ID/retry" \
-H "X-API-Key: YOUR_API_KEY"Automatic retries: When your endpoint rejects a delivery (non-2xx), times out from lomi.’s viewpoint, or a transient fault occurs mid-flight, the platform records the failure and may schedule additional attempts depending on HTTP semantics and plumbing. Retries are bounded: they follow exponential backoff policies on the webhook infrastructure, rather than indefinite “fire until success” loops.
Roughly speaking: 4xx client errors are usually terminal for that automated sequence (misconfiguration should be corrected on your side), while transient failures (5xx, timeouts, certain network faults) may be retried up to a bounded number of attempts with exponential backoff. See Webhook reliability: Timeouts and retries for the approximate ~4 second outbound HTTP timer, up to four attempts on some paths, up to five when delivery is queued, and the meaning of lomi_environment.