SAAS PAYMENTS
SaaS Payment Webhooks: Verify, Retry, and Fulfill Safely
Anyway Team
· 16 min read ·

A customer pays for your SaaS plan, closes the checkout tab, and never reaches the success page. If your app depends on that redirect, the customer may never get access. Process the same event twice, and you may grant two credit packs instead.
Use a signed SaaS payment webhook to tell your server that a payment happened. Verify its signature and timestamp against the raw body, store the event durably, and match the paid order to a purchase in your own database. Then grant access once. Retries should help you recover a missed event, not give the customer a second credit pack.

The examples below use Anyway's events, Ed25519 signatures and delivery rules. The architecture is useful elsewhere, but other providers have different payloads and verification schemes. The code is a sketch to explain the flow, not a tested deployment package.
In this guide
Choose the payment event that can grant access
Implement the safe eight-step webhook flow
Handle subscription lifecycle updates
Recover after retries stop
Diagnose common webhook failures
Test the complete path before launch
Which webhook confirms that payment was captured?
For Anyway, start from a verified order.paid event. Match its product, amount, currency and merchant reference to the pending purchase on your server before granting access. order.pending only tells you an order exists. A browser redirect or buyer-edited metadata can't prove the payment happened either. With another provider, use its successful-payment event and verification rules.
With Anyway, you can publish a hosted link for a product or subscription and connect the resulting order.paid event to your app. You keep control of which account receives access and how duplicate grants are prevented. The payment-link setup guide covers the checkout side.
Why SaaS products need payment webhooks
The customer leaves your application to complete checkout. Your server still needs to learn whether payment was confirmed, failed, renewed, or canceled.
A return page can't answer that question safely. Buyers can close the tab before returning, redirects can be delayed, and URL values can be changed in the browser. A webhook sends the payment state directly from the commerce system to an HTTPS endpoint controlled by your application.
SaaS teams commonly use payment webhooks to:
activate a paid workspace after the first confirmed order;
add credits after a successful purchase;
issue a license key or download entitlement;
record a subscription and its current paid-through date;
keep access aligned with the paid subscription period;
give support staff a searchable order history;
reconcile internal purchases with external order IDs.
This handler can unlock a paid workspace or add spendable credits. Treat it with the same care as login and account permissions.
The safe webhook flow in eight steps
Use the following sequence for each incoming event:
Read the exact raw request body.
Collect the event ID, timestamp, and signature headers.
Confirm that the timestamp is inside the accepted time window.
Verify the cryptographic signature before parsing or trusting the payload.
Insert the verified event into a durable inbox with a unique event ID.
Queue the business action from that stored event; make the enqueue step retry-safe.
Return a
2xxresponse quickly.Process the business action idempotently and record its result.
Signature verification answers, “Did Anyway sign these exact bytes?” Order validation answers a different question: “Does this paid order match something our application intended to sell?” You need both checks.
1. Register the webhook endpoint
In Anyway, create and manage endpoints under Business profile → Developers → Webhooks. Use a dedicated HTTPS route such as:
Keep this route separate from browser sessions. It should not require a user's login cookie or return an HTML page.
Choose the events the application actually processes. An empty event selection subscribes the endpoint to all supported events, while a focused selection can reduce unrelated traffic.
Anyway currently documents these commerce events:
Event | Application meaning |
|---|---|
| An order exists, but payment isn't confirmed. |
| Payment is confirmed and the order can be considered for fulfillment. |
| The payment attempt failed. |
| A subscription record was created. |
| Its period or scheduled-cancellation details changed. |
| The subscription expired. |
| The subscription reached its final ended state. |
Fulfill a purchase only after a verified order.paid event passes your order checks.
2. Fetch and cache Anyway's signing key
Anyway signs webhook deliveries with Ed25519 under the Standard Webhooks format. Fetch the public signing key from:
The response includes a prefixed public key and an equivalent JWKS representation. Cache the key so every webhook avoids an extra network request, and refresh the cached set when Anyway rotates signing keys.
The signing key is public. Keep application API credentials on the server and separate from the webhook verification key.
3. Preserve the raw request body
Signature verification must use the exact bytes Anyway sent. Don't parse JSON and serialize it again before checking the signature. Whitespace, object-key order, and encoding changes can produce different bytes even when the parsed data appears identical.
Configure the webhook route to expose a raw byte buffer. In an Express application, for example, this route should use express.raw({ type: "application/json" }) before any general JSON-body middleware changes the payload.
After the signature is valid, parse the raw bytes as JSON.
4. Verify the timestamp and Ed25519 signature
Anyway sends three important headers:
Header | Purpose |
|---|---|
| Stable logical event ID and idempotency key. |
| Unix timestamp in seconds. |
| Versioned Ed25519 signature in Standard Webhooks format. |
The signed byte sequence is:
Anyway's reference implementation verifies that the timestamp is within 300 seconds of the server's current time. Keep the application server's clock synchronized for this recency check.
The signature header may contain one or more versioned signatures. Check supported v1a signatures against the cached Ed25519 public keys. If none verifies, return an authentication error and don't enqueue or process the event.
Here's the shape of the handler. It isn't copy-paste-ready: verifyAnywaySignature and storeAndQueueOnce stand in for the real verification and durable-storage implementations. The short example makes the order of operations visible:
Before deploying, replace those stubs using Anyway's webhook reference, including its full verification code, header handling and key format.
5. Make processing idempotent
Anyway uses at-least-once delivery so a logical event can receive another delivery opportunity when needed.
Use webhook-id as the event-level idempotency key. Insert the verified payload into a durable inbox table with a unique constraint:
Field | Example |
|---|---|
|
|
| value from |
|
|
| server timestamp |
| nullable server timestamp |
|
|
Here's an easy failure to miss: the inbox insert succeeds, but the queue write fails. On retry, your code sees an event it already knows and skips it. The customer paid, yet nobody grants access. Make that gap recoverable with a transactional outbox or a worker that polls unprocessed inbox rows. An event being “seen” isn't the same as its work being finished.
Event deduplication is only the first layer. Make the business action idempotent too. An entitlement table can enforce one grant per order ID, while a credit ledger can enforce one entry per paid order. Commit the ledger entry and balance change in one database transaction; a check followed by an unprotected increment can still race. For external provisioning, use the downstream service's idempotency mechanism or a persisted operation record with reconciliation. At-least-once delivery doesn't itself guarantee exactly-once business effects.
6. Validate the order against your own record
A valid signature tells you Anyway sent the payload. It doesn't make a buyer's URL parameters trustworthy. Find the purchase in your own database and let that record decide what the account bought.
When creating a checkout, generate a pending purchase in your own database and append its opaque ID as merchant_reference to the SaaS payment link. Store the authenticated account, expected product, and permitted amount on the server. A reference carried in a buyer-visible URL is a lookup hint, not proof of account ownership. When order.paid arrives, load that purchase and compare:
organization ID;
order status;
internal purchase reference;
expected product ID;
the relevant amount in minor units, using the same subtotal/total definition and the documented treatment of discounts and tax;
currency;
whether the purchase has already been fulfilled.
The payload can also contain merchantMetadata, customer information, a payment-link ID and provider details. Buyer-visible metadata can help locate a purchase, but it can't choose a plan, credit amount or account role. If the records disagree, hold the access grant and create an exception support can investigate. Keep the paid order visible instead of silently dropping it or retrying the same mismatch forever.
Store the Anyway orderId on the internal purchase. It gives engineering, finance, and customer support a shared reference when they need to trace a transaction.
7. Queue work and answer quickly
Once the event is verified and stored durably, schedule it for processing and return a 2xx response. A worker can pick up an unprocessed inbox row even if a queue publish fails. Don't wait for email delivery, PDF generation, a large database migration, or a third-party provisioning call inside the webhook request.
Anyway documents a 10-second response window and up to 12 delivery attempts for retryable responses. Exponential backoff and Retry-After support give the endpoint multiple opportunities to receive the event while the queue worker handles the product operation separately.
The response code matters. Anyway retries transport failures, 408, 429, and 5xx; it doesn't retry 3xx or other permanent 4xx responses. Retry-After is honored for 429 and 503, up to a one-hour cap. Don't turn a temporary storage outage into a permanent client error, or return success before durable receipt.
The 2xx response means you've safely received the event. Your worker still has to deliver what the customer bought.
8. Preserve the newest lifecycle state
Webhook systems can deliver lifecycle updates in a different sequence from their creation time. Use a documented resource version or record update time to preserve the newest state. The webhook-timestamp header is for delivery verification; it isn't a safe substitute for the order or subscription's update time. If the payload can't establish ordering, fetch the current resource through the authenticated API before changing access.
Use monotonic state rules:
never regress a locally paid order to pending;
compare documented resource versions or update times before overwriting newer data;
store the final
subscription.endedstate as final unless a documented new lifecycle begins;derive access from confirmed payment and paid-through time, not arrival order alone;
retain an event history for debugging without treating every arrival as the current truth.
If an old “pending” update arrives after “paid,” it belongs in the event history, not on top of the customer's current access state.
Subscription webhook handling
Subscription events use data.subscription. Useful fields include the Anyway subscription ID, customer ID, status, product, billing interval, amount, currency, current period end, and cancellation state.
When a customer schedules cancellation at the end of a paid period, Anyway reports the change through subscription.updated. The cancelAtPeriodEnd field indicates that cancellation is scheduled, and currentPeriodEnd gives the current paid-through time when available.
Your application can keep access active through that paid period. When subscription.ended arrives and the final state is confirmed, update the local subscription and apply the product's access rules.
Don't expect merchantReference or merchantMetadata inside data.subscription; those fields belong to order payloads. Use subscriptionId to connect the subscription to its related orders when the initial checkout context is needed.
For the broader billing setup, read how to accept global subscription payments.
Keep focused operational logs
Good webhook logs help answer four questions:
Which logical event arrived?
Was its signature valid?
Was it new or already seen?
Which internal purchase or subscription did it update?
Record event IDs, event types, order or subscription IDs, processing status, timing, and redacted error codes. Limit access to these logs and define a retention period. Don't log API keys, full signature headers, or unredacted customer payloads.
Keep logs focused on operational identifiers and processing results. Redact customer metadata before sending diagnostic events to external monitoring or chat tools.
Recover when delivery or fulfillment retries are exhausted
Provider delivery retries and your worker's processing retries are separate. Once your endpoint acknowledges receipt, a later provisioning failure needs your own recovery path. Anyway's documented retry limit also means a prolonged endpoint outage can't be solved by waiting indefinitely.
Use two queues of exceptions: undelivered or missing payment updates, and received events whose business action failed. Alert on old unprocessed inbox rows and repeated worker failures, with a support-visible order reference.
Retrieve the current order or subscription through the authenticated Merchant API. Verify organization, product, amount, currency, and the server-side purchase association.
Compare that state with your local access or credit ledger. A paid order with no grant is a recovery candidate; a grant that already exists isn't a reason to grant again.
Enqueue the same idempotent fulfillment operation with an audit record of the recovery. Don't bypass signature checks on a public webhook route to imitate a provider event.
Confirm the ledger and entitlement after processing, then close the exception. Escalate missing or contradictory records for review rather than guessing.
Run reconciliation often enough to catch missing access before customers have to ask, while staying within API limits. Build this recovery into your application; the sequence above isn't an automatic Anyway replay service.
SaaS payment webhook troubleshooting
Symptom | Likely cause | What to check |
|---|---|---|
Valid events fail signature verification | JSON was parsed or serialized before verification | Verify the exact raw request bytes and the three webhook headers. |
A customer receives access twice | The handler lacks event or business-level idempotency | Add a unique constraint on |
Access appears before payment is confirmed | The app trusts | Grant access only after a verified |
Deliveries time out and retry | Email, provisioning, or another slow task runs inside the request | Store or queue the event, return |
A paid order returns to pending | Events arrived out of order | Compare resource versions/update times, or fetch current state; don't sort by delivery time. |
A verified payment never grants access | Work was lost after acknowledgement, or retries stopped | Inspect the durable inbox, worker failures, and reconciliation exceptions. |
A correct payment is rejected as an amount mismatch | Subtotal, tax, discount, or currency definitions differ | Compare like-for-like fields and preserve the order for review without granting access. |
Testing before launch
Run these cases in a non-production environment before accepting live customers:
Test | Required result |
|---|---|
A valid paid order matches a pending purchase | The correct account receives its entitlement once. |
The signature is missing, invalid, or covers a body that was changed | Verification fails before any business action. |
The timestamp is outside the accepted window, including excessive future skew | The request is rejected; server clock health is checked. |
The same logical event arrives repeatedly or concurrently | One inbox identity and one business effect remain. |
Two workers race to grant the same paid order | Database constraints and atomic ledger updates prevent duplicate credits. |
Product, organization, currency, amount, or purchase association mismatches | No access is granted; a traceable exception is raised. |
Durable storage fails before acknowledgement | The endpoint doesn't falsely acknowledge receipt; failure is retryable. |
Queue publishing fails after the inbox row is committed | An outbox or polling worker recovers the stored event. |
Provisioning fails after a successful webhook response | Application retries recover it without a second grant. |
An old lifecycle update arrives after a newer one | Current confirmed state is preserved or refreshed through the API. |
Cancellation is scheduled for the period end | Access follows the confirmed paid-through period and product policy. |
Delivery or worker retries are exhausted | Alerting and reconciliation recover the order through the idempotent path. |
Check the success-path response time, unsupported event handling, and log redaction. An authenticated but unused event should be handled deliberately without granting access. Customer support should also be able to locate a purchase by the internal reference and Anyway order ID when recovering access.
If the product also handles disputes, test whether a support agent can trace the order and retrieve the evidence described in the SaaS chargeback guide.
Why Anyway fits this SaaS flow
Anyway gives you the hosted payment link, signed payment event and order record used in this guide, plus subscription lifecycle events and Merchant API access. Your app turns those records into product access and customer support. For eligible covered sales, Anyway takes the legal-seller role. Its MoR documentation also states a current limit: automated per-order tax calculation, collection and remittance hasn't launched. Resolve the tax treatment your sales need separately from this webhook integration.
Eligible independent developers can apply without first creating an overseas company or overseas bank account. Once approved, you can start with one product link and carry an internal purchase reference through checkout. Whether a person or an AI agent buys, your backend can use the same verified paid-order path to grant access.
The published platform price is 3.9% + $0.50 per successful transaction, with no monthly or setup fee. See Merchant of Record fees for price examples and local bank payouts for SaaS founders for the documented payout options.
Ready to connect checkout to your app? Check merchant eligibility and create an Anyway account, then implement and test the paid-order flow before accepting live purchases.
Frequently asked questions
Which payment webhook event means the payment was captured?
For Anyway, use a verified order.paid event and match the order to your own pending purchase before granting access. Other providers use different event names. Don't activate from order.pending, a browser redirect, or buyer-visible metadata alone.
Why must I verify the raw body?
The signature covers the exact bytes sent by Anyway. Parsing and serializing JSON can change those bytes, which causes verification to fail or encourages an incorrect implementation.
What is the idempotency key for an Anyway webhook?
Use the webhook-id header. It remains stable across retries of the same logical event. Add a unique database constraint and also make the resulting entitlement or ledger operation idempotent.
How quickly should the endpoint respond?
Return a 2xx response after the verified event has been stored or queued durably. Anyway documents a 10-second response window, making a background worker the right place for long-running fulfillment.
How should I preserve the newest event state?
Compare documented resource versions or update times, not the delivery timestamp. When event ordering is ambiguous, retrieve current state through the authenticated API and reconcile your local record before changing access.
Should API keys be used to verify webhook signatures?
No. Anyway webhook verification uses its public Ed25519 signing key and the signed request headers plus raw body. API keys are for authenticated API requests and should remain private.
Sources
You might also like
Payment Links for SaaS: Launch Checkout Without Building One — connect the hosted checkout to the purchase record used in this guide.
How to Accept Global Subscription Payments for SaaS — map paid periods, renewal events, and access.
AI Credits vs Usage-Based Billing — design the ledger that receives a verified credit purchase.