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.
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.
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.
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 terminalENDPOINT_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
TheOrsunPay-Signature header has this form:
verify-webhook.mjs:
verify-webhook.mjs
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; thecallbackUrl 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.