Skip to content

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 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.

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.

{
"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).
{
"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.

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}.

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.

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.

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 request
deliveryId = header["X-Paytalya-Delivery-Id"]
expected = "sha256=" + lowercase_hex( HMAC_SHA256(webhookSecret, rawBody) )
# 1) signature must match (constant-time comparison): over the raw body only
if 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 GET
markProcessed(deliveryId)
return 200
  • Return 2xx fast. Your endpoint must respond HTTP 2xx within 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-Id may 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/payments and base your decision on the query result.
  • Success: HTTP 2xx. The delivery is considered complete.

  • Permanent failure: a 4xx client error → not retried. Make sure your endpoint returns 2xx for valid deliveries.

  • Retry: 5xx responses and timeouts → retried with exponential backoff:

    30s → 2m → 10m → 1h → 6h

    If 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/payments result, not from the webhook type.

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.