Skip to main content
In live mode, the sale or payout’s callbackUrl is a destination for status events. In test mode, configure the separate test endpoint in the cabinet. Use a public HTTPS URL (default port 443) without URL credentials; private/local addresses and redirects are rejected. Ask your operations contact about any deployment-specific allowed ports. Each merchant has its own signing secret, issued separately from the API key. Store it in your secret manager. A webhook is never intentionally sent unsigned. Rotate the secret in the merchant cabinet (Webhooks, MFA required; the new secret is shown once) or through your operator, and update your receiver at the same time. Rotation takes effect immediately: subsequent attempts use only the new secret, including retries of older events. There is no dual signing or sender-side grace period. Do not call internal administrative endpoints with your merchant API key.
This is a shortened synthetic event. data may contain additional fields. Amounts are decimal strings. Deliveries are at least once and can arrive out of order: deduplicate by the signed body’s id, not transaction ID or signature. Event ID and created_at stay stable on retries; the signature timestamp is refreshed for each delivery attempt.

Event types

  • transaction.updated: a sale or payout changed status.
  • refund.succeeded: a refund completed successfully.
  • refund.failed: a refund failed.
In live mode, refund events use the original sale’s callbackUrl, falling back to the merchant’s configured default webhook URL. They use the same signature verification example and event-ID deduplication as payment events.
For refund.failed, refund.status is FAILED; errorCode may contain a failure code. refundedAmount totals only successful refunds and never decreases: SUCCEEDED is terminal. When events arrive out of order, retain the maximum total using decimal arithmetic. refundState is none, partial, or full. All amounts are decimal strings. The transaction summary is a snapshot, not a live balance: later refunds do not change an earlier event’s body. Use the event’s refund ID to reconcile each refund and do not replace newer totals with an older delivery. Refund envelopes also expose created as an ISO-8601 UTC alias of created_at; payment envelopes keep their existing shape. The body and destination remain fixed across retries and redrives. No webhook event is created when neither the sale nor the merchant has a callback URL. Use the signed api_version to identify the payload version. The event catalogue also covers cashier wallet effects and the receiver test event.

Temporarily disabled destinations

A destination can be disabled after sustained delivery failure: by default its first dead event must be older than five days, at least three distinct events must be dead in the last five days, and no delivery may have succeeded in that window. The thresholds are configurable. Further deliveries to that destination become terminal ENDPOINT_DISABLED without an outbound request. The cabinet Webhooks page shows the host and disable date. A user with webhook settings permission can enable it again, optionally requeueing up to 1000 dead events from the disable window. The checkbox starts unchecked. Redrives preserve event IDs, so continue deduplicating deliveries. A new callback URL has separate health; host-only notices are emailed to eligible company users.

Verify the signature

The OrsunPay-Signature header has this form:
The signature is HMAC-SHA256 over the ASCII timestamp, a literal period, and the exact raw body bytes, using the full issued secret string as the UTF-8 key. Do not decode the secret’s hex suffix, parse and reserialize JSON, trim whitespace or otherwise change the body before verification. Reject timestamps more than 300 seconds from your clock (past or future); keep your clock synchronized. Use a constant-time comparison for equal-length signature bytes. Download dependency-free verification examples for Node.js, PHP 8 (64-bit), or Python 3.8+. These use the same raw-byte signature and 300-second past/future window. PHP uses a conservative 4096-byte header limit; Node and Python limit the header to 4096 UTF-16 units. The Node.js verifier below can be used without the SDK. Save it as verify-webhook.mjs:
verify-webhook.mjs
At the HTTP boundary, capture the unmodified body as a Buffer before your JSON parser. Enforce a request-size limit and reject duplicate OrsunPay-Signature header lines before calling the helper. Pass the single header value and your current secret as [process.env.ORSUNPAY_WEBHOOK_SECRET]; reject failed verification before parsing or performing any business action. The helper takes a Buffer deliberately: an already-parsed object cannot authenticate the original request. After verification, parse and validate the envelope, check the transaction belongs to your merchant and order, then atomically store the event ID and enqueue or apply its effects in your own database. Return 2xx only after durable acceptance; an already-processed valid duplicate can also return 2xx. Do not rely on arrival order, overwrite a newer state with an older event, or fulfill twice. OrsunPay-Event-Id and OrsunPay-Event-Type are convenience headers; use the authenticated body for decisions because the HMAC does not cover those headers. Non-2xx responses and network failures are retried with exponential backoff for approximately 31 hours under the default schedule; exhausted deliveries need operator redrive. Redrives reuse the event ID, so retain your deduplication records beyond the normal retry window. Never log raw webhook bodies, signatures or signing secrets.

Test and live events

Live and test mode have separate webhook endpoints and signing secrets, configured per mode in the cabinet. Test-mode events are delivered only to the test endpoint, signed with the test secret; the callbackUrl of an individual test transaction cannot override that destination, and without a test endpoint no test event is delivered. Live events use the sale’s callbackUrl (refund events fall back to the default URL, as described above). The signed envelope includes livemode: false for sandbox transactions, true for live ones. It is copied from the transaction snapshot and stays unchanged on retries and redelivery. Separate test/live processing using this signed field after verification. Older queued snapshots without a mode use the live default; transaction responses also expose livemode.

Test event: test.ping

From the merchant cabinet’s Webhooks page in live mode, save a default URL and choose Send test webhook to check connectivity and signature verification before a payment. The body has the normal signed envelope and type: "test.ping", with data: { "merchantId": "mr_example", "test": true }. Verify OrsunPay-Signature against the exact raw body with the same algorithm as payment events, then return 2xx. Perform no business actions for a test event: do not create, fulfil or change an order. data.test and type identify the ping; the envelope’s existing livemode field is not the test-ping discriminator. The cabinet shows HTTP status, duration, a safe error class and a journal link. Test events are sent once and never automatically retried. Manual redelivery of the same test event is disabled; sending another test creates a new event ID. If the request times out or recording fails, the receiver may already have accepted the ping: check receiver logs before sending again. Tests share the shop’s manual webhook limit; 429 includes a retry delay. No default URL gives 409.