All posts
No-Code & Tools

AI-Built App Payments: How to Stop Webhook Retries From Creating Duplicate Orders

A practical production checklist for making Stripe, Paystack and similar payment webhooks safe against retries, duplicate fulfilment and false payment state.

Elvis Onunwa20 September 20267 min readLast updated: 20 September 2026
AI-Built App Payments: How to Stop Webhook Retries From Creating Duplicate Orders

TL;DR

  • A successful checkout screen is not proof that your payment workflow is production-safe.
  • Payment providers can retry webhook events, so processing the same event twice must not create a second order, credit, receipt or fulfilment action.
  • Verify the provider signature and server-side payment state before granting value, and use a unique transaction or event identifier to make processing idempotent.
  • Test retries, failures and delayed events before real customers depend on the integration.
On this page⌄

AI builders can make a checkout flow look finished surprisingly quickly. A customer clicks Pay, the provider opens, payment succeeds, the app returns to a success screen, and a row appears in the database.

That proves the happy path works. It does not prove the payment system is safe.

The production question is harder:

What happens when the payment provider sends the same event again, your function times out after updating the database, the browser never returns to your success page, or a customer refreshes at exactly the wrong moment?

Those are the paths that decide whether one payment creates one order or two.

The rule: payment confirmation must be repeat-safe

Payment providers use webhooks because the customer’s browser is not a reliable source of truth. A browser can close, lose connectivity or never reach your return URL even though money moved.

But webhook delivery is not a one-shot promise either. Paystack’s current webhook tooling explicitly supports retries and warns that endpoints can occasionally receive the same event more than once. It recommends checking a unique transaction reference or resource ID before giving value again.

Source: https://paystack.com/docs/payments/webhooks/

Paystack’s dashboard guidance makes the operational consequence even clearer: retrying sends the event again, so the integration must safely handle repeated events without crediting, fulfilling or updating the customer twice.

Source: https://support.paystack.com/en/articles/10609026

So the correct mental model is not:

webhook received → create order

It is:

trusted event received → identify transaction → check whether its business effect already happened → apply the effect once → record the result → acknowledge safely

That property is usually called idempotency.

The duplicate-order failure mode

Imagine an AI-built store receives a successful payment event and does four things:

  1. creates an order;
  2. marks the customer’s purchase as paid;
  3. reduces stock;
  4. sends fulfilment instructions.

The handler completes steps 1–3, but the request fails before the payment provider receives a successful response.

The provider retries.

If the code simply runs the four steps again, one payment can now produce two orders, reduce stock twice or trigger duplicate fulfilment.

The interface may still look perfectly normal.

That is why payment reliability is a state-management problem, not a checkout-button problem.

Do not trust the browser to declare payment success

A success URL is useful for telling the customer what happens next. It should not be the authority that grants value.

Paystack recommends listening for the payment event through a webhook and also provides a server-side transaction verification API. Its documentation specifically says payment should be confirmed before delivering services to the customer.

Source: https://paystack.com/docs/payments/verify-payments/

For a paid app, ecommerce store or booking product, this usually means the server owns the transition from something like:

pending → paid

The browser can display that state. It should not invent it.

Verify who sent the webhook

Webhook URLs need to be reachable by the payment provider, which means they are exposed to the internet.

Public does not mean unauthenticated in the business sense.

Paystack signs webhook events with an x-paystack-signature HMAC and documents verifying that signature before processing the event. Stripe uses its own signed webhook mechanism.

If you use Supabase Edge Functions, this distinction matters. Supabase documents external webhooks as a case where the function may skip your application’s normal JWT requirement because the external provider does not possess a user token. The handler should then verify the provider’s signature itself.

Sources: https://paystack.com/docs/payments/webhooks/ https://supabase.com/docs/guides/functions/auth

A common AI-generated mistake is to solve the wrong security problem: either the webhook rejects every legitimate provider request because it expects an app-user JWT, or the endpoint is made public and the provider signature is never checked.

Both are wrong.

Make the business effect unique, not just the HTTP request

The safest place to prevent duplicate fulfilment is usually the data layer.

Suppose a provider transaction has reference TX-123.

Your system should have a durable way to say:

The successful business effect for TX-123 has already been applied.

That can be enforced with a unique payment/event record and a transaction that ties the payment record to the state change it authorizes.

The exact schema depends on the product. The principle does not.

If two webhook requests race each other, both should not be able to independently decide that the payment is new.

This is one reason I treat payments as a workflow with explicit states rather than a button plus a callback. Once money changes business records, the important question becomes what transitions are allowed, who can trigger them, and can the same transition happen twice?

Keep payment state separate from fulfilment state

Another useful design choice is to avoid one overloaded status field trying to describe everything.

A payment can be successful while fulfilment is still pending. An order can later be refunded. A subscription can have a different lifecycle again.

For example:

Concern Example states
Payment pending, successful, failed, refunded
Order created, confirmed, cancelled
Fulfilment pending, processing, completed

This separation makes retries and recovery easier to reason about.

It also makes support easier. When a customer says, “I paid but did not receive access,” you can determine whether payment confirmation failed or fulfilment failed after a valid payment.

Test the failure paths before going live

Do not stop testing when one test card succeeds.

Before customers depend on the integration, deliberately test:

  • the same successful webhook arriving twice;
  • a delayed webhook arriving after the customer leaves the page;
  • an invalid signature;
  • a failed or abandoned payment;
  • your handler failing after part of its work;
  • a manual webhook retry;
  • a refund or reversal where your product supports it;
  • test and live credentials being kept in the correct environments.

Paystack now exposes webhook delivery history, responses and manual retry controls in its dashboard, which makes this kind of testing and diagnosis easier.

If you are using Supabase, Edge Functions are explicitly designed for webhook receivers and third-party integrations, and Supabase advises designing short-lived functions for idempotent operations.

Sources: https://support.paystack.com/en/articles/10609026 https://supabase.com/docs/guides/functions

The minimum production-safe payment model

For a small MVP, you do not need a giant payments architecture. You do need a disciplined one.

A reasonable minimum is:

  1. initialize payments from trusted server-side logic where appropriate;
  2. keep secret keys out of browser code;
  3. receive payment-provider events on a server endpoint;
  4. verify the event signature;
  5. verify the transaction and expected amount/reference where the provider flow requires it;
  6. enforce a unique transaction/event identifier;
  7. update payment and business state atomically where practical;
  8. make retries harmless;
  9. log enough context to investigate failures;
  10. test duplicate and failure paths before launch.

The important word is business state.

A webhook returning 200 is not the outcome you are protecting. The outcome is that the right customer receives the right value exactly when a legitimate payment justifies it, without the same event applying that value twice.

AI can build the integration. Someone still has to reason about the system

This is the recurring gap with AI-assisted development.

AI is very good at connecting a checkout component, an Edge Function and a database table. The harder work is deciding what the source of truth is, how state changes, what happens when a step fails, and how the system recovers.

Those questions are not reasons to avoid AI builders. They are the reason to add engineering and product judgment before the prototype becomes a business-critical system.

If your AI-built app already accepts payments and you are unsure whether retries, fulfilment and payment state are safe, my MVP development service covers the production layer around working prototypes. You can also contact me with the current app, repository or payment flow and I can help determine whether it needs a focused fix or a broader production-readiness pass.

For the wider launch review, use my AI-built app production checklist. If you are still testing changes against live users and live payment credentials, fix the environment boundary first with Staging vs Production for AI-Built Apps.

Frequently asked questions

Questions, answered.

Why can a payment webhook arrive more than once?

Payment systems retry delivery when acknowledgement fails or delivery status is uncertain. Your handler should assume an event may be delivered again and make repeated processing harmless.

Is the payment success page enough to mark an order as paid?

No. Treat browser redirects as user experience, not authoritative payment confirmation. Confirm payment server-side using a verified provider event or transaction verification flow.

Should a public webhook endpoint require my app user's JWT?

Usually not. External payment providers do not have your user's application token. The endpoint can be publicly reachable while still authenticating the provider by verifying its signed payload.

What should be unique in the database?

Use the payment provider's stable event, transaction or resource identifier as part of an idempotency rule so the same payment cannot create the same business effect twice.

NEED HELP WITH THE BUILD?

Turn what you learned here into a clearer website or MVP.

Share the goal, current stage, and constraints. I’ll help you identify the smallest responsible next step.

Continue reading

Related notes.