Skip to content

Webhooks

Paytalya sends a webhook to your endpoint as a payment or refund status changes. The payload is thin: only the event type + paymentCode. A webhook is a trigger, not the authoritative result; you fetch the detail and the final status via GET /v1/payments.

Event typeWhen it is sent
payment.capturedCapture approved (payment captured).
payment.failed3D or capture was rejected (payment failed).
payment.expiredPayment expired / 3D abandoned (payment expired).
refund.approvedA refund or void was approved.
refund.declinedA refund or void was declined.

Raw bank events (authorization steps, intermediate states) are not sent as webhooks. For the full list of payment statuses, see Payment Lifecycle.

{
"id": "whd_a1b2c3",
"type": "payment.captured",
"paymentCode": "pay_7Hq2bL",
"occurredAt": "2026-06-14T12:05:11Z"
}
FieldDescription
idDelivery id. Used for idempotency.
typeEvent type (table above).
paymentCodeThe related payment’s code; use it to query the detail.
occurredAtThe instant the event occurred (UTC, ISO 8601).

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 payment to make a decision.

Every delivery carries three signature headers:

HeaderDescription
X-Paytalya-Signaturesha256=<hex>: the HMAC-SHA256 signature of the raw request body.
X-Paytalya-TimestampThe epoch seconds of the instant the event occurred (the same instant as occurredAt in the body, in seconds form). Provided for optional replay hardening; it is not part of the signature. You may reject requests older than 5 minutes (replay protection), but that check is independent of the signature.
X-Paytalya-Delivery-IdDelivery 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
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) (optional) replay hardening: reject events that are too old (INDEPENDENT of the signature)
timestamp = header["X-Paytalya-Timestamp"]
if abs(now() - timestamp) > 5 minutes:
return 400
# signature valid → return 2xx, then query the payment with GET
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 and timestamp 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.