bizinly.
Let’s talk business

DEVELOPERS

Payment webhooks

The endpoint pattern, signature verification and event list behind each online payment provider.

Each online payment provider Bizinly supports posts its own webhook to the same URL pattern, scoped to the workspace and the provider:

POST https://<your-workspace-domain>/api/pay/<provider>/webhook

<provider> is one of stripe, paypal, revolut, gocardless, authorizenet or wise. These endpoints are public — no bearer token or session is expected — because each provider authenticates its own call differently, as below. A request that fails verification gets 400; the provider retries it on its own schedule. A request that verifies but describes an event Bizinly doesn't act on gets 200 — there's no need to hand-pick a narrower event subscription on the provider's side, unrecognised event types are simply ignored.

Verification, per provider

Provider How it's verified
Stripe Stripe-Signature header (t=<timestamp>,v1=<hex>); Bizinly recomputes an HMAC-SHA256 of <timestamp>.<raw body> with the endpoint's signing secret and compares it, rejecting a timestamp too far from now (replay protection).
Revolut Revolut-Signature (v1=<hex>) and Revolut-Request-Timestamp headers; HMAC-SHA256 of v1.<timestamp>.<raw body> with the webhook signing secret, same timestamp tolerance.
GoCardless Webhook-Signature header; HMAC-SHA256 (hex) of the raw body with the webhook endpoint secret.
Authorize.Net X-ANET-Signature header (sha512=<hex>); HMAC-SHA512 of the raw body with the account's Signature Key.
PayPal No local signature check — the raw event, its headers and the webhook ID are sent to PayPal's own verify-webhook-signature API, and only a SUCCESS response is trusted.
Wise The payload is never trusted for its contents — receiving any webhook for a payment request is only a hint to re-read that request from Wise's own API, and only what the API returns decides whether it's paid.

All raw-body HMAC comparisons are constant-time. A missing or unconfigured secret (or webhook ID, for PayPal) always fails closed rather than skipping the check.

Idempotency — what's safe to retry

Every webhook resolves to a specific transaction: a checkout Bizinly itself created earlier for one document and one provider reference. Settling that transaction into a recorded payment only happens once — a transaction already settled is left alone, whichever call reaches it. In practice that means:

  • A provider re-sending the same event (their retry policy, or you manually resending it from their dashboard) is always safe — it settles nothing a second time.
  • The webhook and the return-URL the payer lands on after paying race for the same transaction; whichever arrives first settles it, the other is a no-op.
  • Bizinly's own periodic reconciliation, which re-asks each provider about any transaction still awaiting confirmation, can run at any time without risk of double-settling — it uses the same settlement path as the webhook.

An event for a transaction Bizinly doesn't recognise (wrong workspace, or one it never created) is acknowledged and otherwise ignored, not treated as an error.

Events acted on

Provider Events
Stripe checkout.session.completed, checkout.session.async_payment_succeeded → paid · checkout.session.async_payment_failed → failed · checkout.session.expired → expired
PayPal PAYMENT.CAPTURE.COMPLETED → paid · PAYMENT.CAPTURE.REFUNDED → refunded · CHECKOUT.ORDER.VOIDED → cancelled
Revolut ORDER_COMPLETED → paid · ORDER_CANCELLED → cancelled · ORDER_PAYMENT_DECLINED, ORDER_PAYMENT_FAILED → failed
GoCardless payment confirmed, paid_out → paid · payment failed, cancelled → failed/cancelled · payment charged_back → refunded · a billing request turning fulfilled → re-read, to pick up the payment it created
Authorize.Net net.authorize.payment.authcapture.created, net.authorize.payment.fraud.approved → re-read the transaction and settle if captured · net.authorize.payment.fraud.declined → failed · net.authorize.payment.void.created → cancelled · net.authorize.payment.refund.created → refunded
Wise any event naming a payment request → re-read that request

If a webhook isn't registered yet

Nothing is stuck waiting on it. Bizinly periodically re-asks each provider about a transaction that's still awaiting confirmation, so a payment still completes and the invoice still gets marked paid — a registered webhook just makes that happen immediately instead of on the next check.

Related: Online payments, Overview & architecture.

Book a live demo Back to the docs