Webhooks
POST to your endpoint on customer events: signature, retries, delivery log.
We POST to your endpoint when something happens to your customers. Configured in the dashboard under "Webhooks".
Events
| Event | When |
|---|---|
account.created | you created a customer — via the API or your storefront |
account.suspended | a customer was switched off |
account.restored | a customer was switched back on |
payment.succeeded | a confirmed payment by your customer |
payment.refunded | a refund or chargeback of such a payment |
A subscription is any subset of that list. An event outside it is rejected at registration: a typo in payment.suceeded has to fail immediately rather than become a subscription that never fires.
There is also webhook.test, which cannot be subscribed to — it arrives only from the "Send test event" button in the dashboard.
The body
POST <your url>
Content-Type: application/json
X-Logrus-Event: payment.succeeded
X-Logrus-Delivery: 6f1d...
X-Logrus-Idempotency-Key: payment.succeeded:6b0c...
X-Logrus-Signature: sha256=<hex>
{
"event": "payment.succeeded",
"idempotency_key": "payment.succeeded:6b0c...",
"created_at": "2026-09-01T10:00:00Z",
"data": {
"payment_id": "6b0c...",
"account_id": "9a1e...",
"subscription_id": "2f77...",
"plan_code": "base",
"amount_cents": 29900,
"currency": "RUB",
"status": "succeeded",
"occurred_at": "2026-09-01T10:00:00Z"
}
}
amount_cents is always in its currency's minor units and currency always travels with it. Never read the amount without the currency.
For account.* the data block carries account_id, email, external_ref and status.
Signature
X-Logrus-Signature is sha256= followed by the HMAC-SHA256 of the raw request body, lowercase hex, keyed with your endpoint's secret. The secret is shown once at creation and once at each rotation.
Verify before acting on the event, with a constant-time comparison:
import hmac, hashlib
def verify(secret: str, raw_body: bytes, header: str) -> bool:
mac = hmac.new(secret.encode(), raw_body, hashlib.sha256)
return hmac.compare_digest("sha256=" + mac.hexdigest(), header)
Hash the bytes you received. If your framework parsed the JSON first and you re-serialise it, the signature will not match, and the HMAC will not be what is wrong.
Idempotency
idempotency_key is stable for the fact, not the attempt: retries and a re-emission of the same event carry the same key. Store it and skip duplicates and nothing is ever applied twice.
Retries
Any 2xx counts as success. Otherwise:
- 5xx, timeout, connection error — retried after 1, 2, 4, 8, 16, 32, 64, 128 minutes: eight attempts, a little over four hours in total. After that the delivery is marked
dead. - 4xx (other than 408 and 429) — not retried. That answer means the endpoint understood us and refused, and four hours will not change it.
- 408 and 429 are retried as temporary.
Answer fast: the timeout is 10 seconds. Put the event on your own queue and return 200; process it afterwards.
Auto-disable
After 20 consecutive failed deliveries the endpoint is switched off, with the reason visible in the dashboard. Fix it and switch it back on — the counter resets.
The log
In the dashboard, under the endpoint list: event, status, attempt count, response code and when the next attempt is due. It is also the only way to see our side of the conversation without asking us.
What a webhook does not replace
A notification can be late, or (after eight failures) never arrive. If reconciliation matters to you, poll metrics and payouts periodically. Webhooks are fast, polling is reliable; serious integrations do both.