Back to blogTechnology

Payments in Vibe Coding: From a Checkout That Works to a System You Can Trust

The charge went through, but the order still shows as unpaid — or the other way round. Here's how to build a secure, consistent payment flow that handles duplicate events in an AI-built application.

The user saw “Payment successful”, but your system still marks the order as pending. They click again — and now there are two charges. This can happen when a payment flow treats a payment as a single API call and leaves retries and duplicate requests unhandled.

In practice, a payment is a distributed process: the browser, your server, the payment provider, a webhook, and sometimes an invoicing or inventory system all have to agree on the same state — even when a request is delayed, sent twice, or arrives out of order.

First rule: the browser is not the source of truth

You must not deliver a product, activate a subscription, or mark an order as paid simply because the user landed on /success. That URL can be opened manually, the window can be closed before the redirect, and data stored on the client can be changed.

The source of truth has to be a verified confirmation from the payment provider, received and processed on the server. The browser displays state; it does not determine it.

A correct basic flow looks like this:

  1. The server creates an internal order in a pending state.
  2. The server calculates the price from a trusted catalogue — not from an amount sent by the browser.
  3. The server creates a payment session, such as a Stripe Checkout Session, with the payment provider.
  4. The user completes payment on the provider’s secure page.
  5. The provider sends a signed webhook to the server.
  6. The server verifies the signature, the link to the order, the amount, the currency, and the payment status.
  7. Once payment is confirmed and duplicate fulfilment is ruled out, the order moves to paid and fulfilment is scheduled.

With delayed payment methods, a completed Checkout Session can still be unpaid. Follow the provider’s fulfilment guidance and wait for payment confirmation before delivering a paid order.

Prefer a payment page hosted by the provider

The less card data passes through your system, the smaller the risk surface. For most Vibe Coding products, a payment page hosted by a compliant payment provider — or a component fully supplied by it — is a better default than a card form written by AI.

According to the PCI Security Standards Council, eligibility for the SAQ A path depends, among other things, on all elements of the payment page originating directly from a PCI DSS compliant service provider, and on all other eligibility criteria being met. A single element that collects card data and is served from your own site may change the scope of your responsibility.

This isn’t only a compliance question. A custom form can expose card data through malicious JavaScript, logs, or analytics tools. A hosted page moves a significant portion of the risk and the maintenance outside the boundaries of your application.

An embedded payment form and a redirect to a hosted page have different eligibility conditions. PCI SSC’s script guidance explains the additional check for sites with embedded forms. Confirm the applicable SAQ with your acquirer or other compliance-accepting entity.

Every webhook must pass signature verification

A public endpoint that accepts JSON and updates an order to paid is an open door to forgery. Stripe’s documentation warns that without verification, an attacker can send a forged event and trigger product fulfilment, access grants, or record changes.

Safe handling includes:

  • reading the raw request body as your library’s guidance requires;
  • verifying Stripe-Signature using the endpoint secret;
  • a separate secret per endpoint and per test/live environment;
  • accepting only the event types you actually need;
  • durably recording or queueing the verified event before returning a fast success response, then processing heavy work asynchronously;
  • recording the event identifier for auditing and duplicate prevention.

Never store the webhook secret in the frontend, in the code repository, or in a prompt to an AI tool.

Idempotency: the same event must not perform the same action twice

Networks fail, users click again, and providers resend events. Every financial or business action therefore needs to be idempotent: reprocessing the same request doesn’t change the outcome after the first time.

When creating a payment, use a stable idempotency key for that operation and reuse it when retrying the same request. A new payment attempt or refund needs its own key. The provider’s retention window is limited, so keep your own record of payment attempts too.

For webhooks, record each event identifier and protect the business action against duplicates as well. Two different events can refer to the same payment. Use database constraints and a transaction to prevent concurrent handlers from scheduling fulfilment twice; do not treat every update to the same object as a duplicate.

A schematic example:

verify signature and supported event type
verify successful payment, order reference, amount and currency
begin transaction
  lock order
  if event_id not already processed:
    if payment is eligible and fulfilment is not already scheduled:
      mark payment confirmed
      record a unique fulfilment job in the outbox
    record event_id as processed
commit
return success

This example covers a successful payment. Refunds, disputes, and failed attempts need their own handlers. An outbox worker delivers the fulfilment job with a stable operation identifier so retries do not deliver the product twice. Return 200 for a verified event already handled; if durable storage fails, return a retryable error.

Don’t manage a payment with a boolean

A single field like isPaid: true doesn’t describe reality. A payment can require additional authentication, fail, be cancelled, be partially refunded, or turn into a dispute.

Track payment attempts, refunds, disputes, and fulfilment separately. A simplified business view might include these states; it is not a list of Stripe API statuses:

State Meaning Allowed actions
pending An order was created, no payment confirmation Payment attempt or cancellation
processing The provider is still processing Wait and check only
paid A verified confirmation was received One-time fulfilment
failed The payment attempt failed New attempt
refunded A full refund was issued Stop service per policy
partially_refunded Part of the amount was refunded Accounting and fulfilment adjustment
disputed A dispute was opened Freeze action and review manually

A transition between states needs verified evidence and a recorded reason. Because events can arrive out of order, reconcile an apparent contradiction with the provider’s current record before changing state. Refunds should refer to a confirmed payment, not merely to the latest local status label.

Five common failures in quickly generated code

1. The price comes from the client

The frontend sends price: 49, and the server charges the value it received. An attacker can change it. The server should accept a product identifier, look up the price from an internal source, and validate currency, tax, and discount.

2. Test mode is connected to production data

Test and live keys, webhooks, and products must be kept separate. Check that the webhook secret matches the same endpoint and the same environment in which the event was created.

3. Every webhook is accepted

Listening to every event increases load and complicates the code. Subscribe only to the types you need. If a correctly signed but unsupported event arrives, acknowledge it without changing business state; an error response can trigger unnecessary retries.

4. The confirmation email is sent before the commit

If you sent a confirmation to the customer and then the database transaction failed, you’ve created a gap that is hard to repair. Persist the state change first, and trigger side effects through a queue or a reliable outbox.

5. There is no reconciliation between the provider and the database

A webhook can fail even though the payment went through. Run scheduled reconciliation that compares transactions at the provider with internal orders and reports discrepancies for review.

Tests you must have before launch

Don’t test only a card that succeeds. Your test environment should cover at least:

  • a successful payment with exactly one fulfilment;
  • an identical webhook event arriving twice;
  • events arriving out of order;
  • an invalid signature or a modified request body;
  • an amount or currency that doesn’t match the order;
  • a payment that fails and then succeeds;
  • full and partial refunds;
  • a timeout between your server and the provider;
  • a user who closed the window before the success page;
  • a fulfilment failure after a charge, escalated to a human.

The most important test is the end-to-end business process: money moved, the record was updated, the product was delivered exactly once, and the team can explain what happened.

Payments need a human in the loop

Not every event should be resolved automatically. An unusual amount, a high number of refunds, a reconciliation gap, or a dispute should create a task for a person with all the context they need — without exposing financial information that isn’t necessary.

A Process-First approach separates the decision from the execution: AI can classify a failure reason or prepare a summary, but an exceptional refund, a permission change, or handling a dispute stays inside an approved process with an audit trail.

Production payments checklist

  • Card data is handled on a secure page or component from a compliant provider.
  • Price, currency, and discount are calculated and validated on the server.
  • Webhooks pass signature verification before any change.
  • Every financial action and fulfilment is idempotent.
  • An explicit state machine exists for the order and the payment.
  • The test environment is fully separated from production.
  • Logs contain no card details, secrets, or unnecessary information.
  • Monitoring, alerting, and reconciliation are in place.
  • Exceptional refunds and disputes go through human review.
  • Duplicates, reordering, timeouts, and partial failures were tested.

Summary

A reliable payment flow doesn’t end at the Checkout button. It requires clear trust boundaries, a verified server-side confirmation, duplicate prevention, state management, reconciliation, and a way to handle exceptions.

If you built payments with AI and you’re not sure what happens when a webhook arrives twice, or when the charge succeeds and fulfilment fails, start with the Vibe Coding Rescue guide and the security and privacy guide. To review your current implementation, get in touch about our Vibe Coding Rescue service.


Sources and further reading:

Vibe CodingPaymentsStripeWebhooksApplication SecurityWorkflow

Ready to upgrade your business processes?

Let’s talk. A free initial consultation where we learn your needs and see how technology can serve you.

Start a WhatsApp chatFast response on WhatsApp — no commitment
or
We’ll get back to you within one business day.