Webhooks
Lydia Gate sends a webhook to your endpoint as a payment, refund, payout item, or payment link status changes. The payload is thin: only the event type and the related resource’s reference (depending on the family: paymentCode, payoutBatchId+partnerId, or paymentLinkId). A webhook is a trigger, not the authoritative result; you fetch the detail via the matching query — for payments/refunds via GET /v1/payments, for payment links via GET /v1/payment-links/{id}; you track payout detail from your merchant panel.
Event types
Section titled “Event types”| Event type | Family | When it is sent |
|---|---|---|
payment.captured |
Transaction | Capture approved (payment captured). |
payment.failed |
Transaction | 3D or capture was rejected (payment failed). |
payment.expired |
Transaction | Payment expired / 3D abandoned (payment expired). |
refund.approved |
Transaction | A refund or void was approved. |
refund.declined |
Transaction | A refund or void was declined. |
payout.paid_out |
Payout | A payout item was paid out to the partner (the linked settlements move to PAID_OUT). |
payout.failed |
Payout | A payout item failed (funds did not reach the partner; resolved by ops). |
payment_link.completed |
Payment link | A payment link’s capture was approved (link completed). |
payment_link.expired |
Payment link | A payment link’s window elapsed (link expired). |
webhook.test |
Test | Sent when “Send test webhook” is triggered on the panel; carries no business data — it lets you test your integration (URL reachability + signature verification) without live data. |
Raw bank events (authorization steps, intermediate states) are not sent as webhooks. For the full list of payment statuses, see Payment Lifecycle; for payment link statuses, see Payment Links.
Body (thin payload)
Section titled “Body (thin payload)”The body does not carry the authoritative result or amount; this is intentional. It contains no raw bank response, card data, or any other sensitive field. Always query the related resource to make a decision. The field set varies by family.
Transaction / refund family
Section titled “Transaction / refund family”{ "id": "whd_a1b2c3", "type": "payment.captured", "paymentCode": "pay_7Hq2bL", "occurredAt": "2026-06-14T12:05:11Z"}| Field | Description |
|---|---|
id |
Delivery id. Used for idempotency. |
type |
Event type (table above). |
paymentCode |
The related payment’s code; use it to query the detail. |
occurredAt |
The instant the event occurred (UTC, ISO 8601). |
Payout family
Section titled “Payout family”{ "id": "whd_9f3e21", "type": "payout.paid_out", "payoutBatchId": "pob_4c7a19", "partnerId": "prt_8b2d4f", "occurredAt": "2026-06-15T09:02:33Z"}| Field | Description |
|---|---|
payoutBatchId |
The related payout batch’s identifier. |
partnerId |
The related partner’s identifier. |
paymentCode does not appear in this family’s body; a payout is at the partner×batch level, not a single-payment level. You track its detail from your merchant panel.
Payment link family
Section titled “Payment link family”The payment_link.completed / payment_link.expired body carries paymentLinkId instead of paymentCode:
{ "id": "whd_c9f01e", "type": "payment_link.completed", "paymentLinkId": "01J9K5QATN6M1PXG7VYSD3HZWB", "occurredAt": "2026-06-21T09:15:40Z"}| Field | Description |
|---|---|
paymentLinkId |
The related payment link’s permanent identifier; query its detail via GET /v1/payment-links/{id}. |
Test family
Section titled “Test family”The webhook.test body carries only id, type, and occurredAt; it has no reference to business data:
{ "id": "whd_1a2b3c", "type": "webhook.test", "occurredAt": "2026-06-15T09:02:33Z"}It is triggered by “Send test webhook” on the panel; it lets you test your integration (URL reachability + signature verification) without live data.
Headers
Section titled “Headers”Every delivery carries three signature headers:
| Header | Description |
|---|---|
X-Paytalya-Signature |
sha256=<hex>: the HMAC-SHA256 signature of the raw request body. |
X-Paytalya-Timestamp |
The epoch seconds of the instant the event occurred (occurredAt); it is the same instant as occurredAt in the body. It is not the send time: when the same event is retried, this value stays constant (the original occurredAt). It is not part of the signature. For that reason, do not build a “reject requests older than N minutes” check; retries carry the same old timestamp even hours later, so you would reject legitimate deliveries. For replay protection, use idempotency on X-Paytalya-Delivery-Id instead of the timestamp. |
X-Paytalya-Delivery-Id |
Delivery id (same as id in the body). The same delivery may arrive again; use it for idempotency. |
The secret used for the signature is a webhook secret separate from the API key and is provided to you during onboarding. Never log this secret, send it to the client, or store it in your repository.
Signature verification
Section titled “Signature verification”The signature is computed over the raw request body only; the timestamp is not included in the signed value, and there is no concatenation. Verify it with a constant-time comparison (against timing attacks). Language-agnostic pseudocode:
rawBody = exact bytes received on the requestdeliveryId = header["X-Paytalya-Delivery-Id"]
expected = "sha256=" + lowercase_hex( HMAC_SHA256(webhookSecret, rawBody) )
# 1) signature must match (constant-time comparison): over the raw body onlyif not constantTimeEquals(expected, header["X-Paytalya-Signature"]): return 400
# 2) idempotency + replay protection: if this delivery-id was already processed, do not reprocess# (INDEPENDENT of the signature; do NOT reject as "too old" by timestamp — it kills retries)if alreadyProcessed(deliveryId): return 200 # acknowledge silently; do not re-run business logic
# signature valid → return 2xx, then query the payment with GETmarkProcessed(deliveryId)return 200Endpoint requirements
Section titled “Endpoint requirements”- Return
2xxfast. Your endpoint must respondHTTP 2xxwithin 5 seconds; otherwise the delivery is treated as a timeout and retried. Do heavy work (querying, updating the order) asynchronously after you respond. - Verify the signature. Do not trust the body before verifying the signature with the steps above.
- Be idempotent. The same
X-Paytalya-Delivery-Idmay arrive more than once (the delivery-id is stable across retries). Your processing logic must be resilient to receiving the same delivery twice. - Use it as a trigger. When a notification arrives, query the payment detail via
GET /v1/paymentsand base your decision on the query result.
Retries and delivery
Section titled “Retries and delivery”-
Success:
HTTP 2xx. The delivery is considered complete. -
Permanent failure: a
4xxclient error → not retried. Make sure your endpoint returns2xxfor valid deliveries. -
Retry:
5xxresponses and timeouts → retried with exponential backoff:30s → 2m → 10m → 1h → 6hIf no success is received within 24 hours of the first delivery, the delivery is permanently dropped.
-
No ordering guarantee: Events may not arrive in the order they were sent. Determine status from the
GET /v1/paymentsresult, not from the webhook type.
Configuration
Section titled “Configuration”Your webhook URL and secret are configured on your account during onboarding. If no webhook URL is configured, no notifications are sent; in that case, track payment statuses solely via GET /v1/payments.