Bharat AI Saathi
ServicesIndustriesSetupsHow we workGuidesFounderVPS HostingContactGlobal (USD)
WhatsApp +91 77352 68199
support@bharataisaathi.com
Mon–Sat 10:00–19:00 IST
Agentic Automation · Global catalog

Stripe Payment Form Example With Your Own Backend

A Stripe payment form example with PaymentIntents, server-side confirmation and webhooks, built so an AI assistant can start checkout too.

Agentic Automation

A working Stripe payment form example has three parts: your backend creates a PaymentIntent, the browser confirms it with the client secret, and a signed webhook marks the order paid. If you also want an AI assistant to start checkout, put the same backend behind an MCP tool. This guide shows both paths and the failure cases between them.

The flow in one pass

The browser never decides whether a payment succeeded. Your server does, based on a webhook Stripe signs.

  1. The customer picks a plan or fills in an order. Your frontend calls POST /api/checkout with an order ID, not an amount.
  2. Your server loads the order, reads the price from your own database, and creates a PaymentIntent with that amount.
  3. The server returns client_secret. The frontend mounts the Stripe Payment Element with it and calls stripe.confirmPayment.
  4. Stripe sends payment_intent.succeeded to your webhook endpoint. You verify the signature, look up the order by metadata.order_id, and mark it paid.
  5. The success page reads order status from your server. The redirect after confirmation only tells you the customer came back; it proves nothing about payment.

Most bugs in homemade checkout come from skipping step 4 and trusting step 5.

Where each step runs

Step Runs in Source of truth Common failure
Price lookup Your backend Orders table Amount sent from the browser and tampered with
Create PaymentIntent Your backend Stripe Duplicate intents on double click
Confirm payment Browser, Stripe.js Stripe 3D Secure abandoned, card declined
Mark order paid Webhook handler Stripe event Parsed JSON body breaks signature check
Show receipt Your backend Orders table Success page trusts the redirect URL
Start checkout from an assistant MCP tool, calls your backend Same orders table Tool accepts a free-form amount

The last row matters if you plan to add an assistant later. It should use the same endpoints as your UI, not a paraloel code path.

Step-by-step setup

  1. Create a Stripe account and use test mode keys. Store the secret key and the webhook signing secret as environment variables on the server only.
  2. Add an orders table with id, amount_cents, currency, status (draft, pending, paid, failed), intent_id and customer_email.
  3. Build POST /api/checkout as shown in the next section. Pass an idempotency key derived from the order ID so a double click returns the same intent.
  4. On the frontend, fetch the client secret, mount the Payment Element, and call stripe.confirmPayment with a return_url that points to your status page.
  5. Add POST /webhooks/stripe. Register the URL in the Stripe dashboard and subscribe to payment_intent.succeeded and payment_intent.payment_failed.
  6. Forward events locally with the Stripe CLI while you develop, and run a test card through both a successful and a declined path.
  7. Make the handler idempotent. Stripe retries events, so if the order is already paid, return 200 and do nothing.
  8. Switch to live keys, register a separate live webhook endpoint with its own signing secret, and place one real small order before launch.

Server code: create the intent and handle the webhook

This is a minimal Express version. The same shape works in any framework.

app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      req.headers["stripe-signature"],
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.sendStatus(400);
  }

  if (event.type === "payment_intent.succeeded") {
    const intent = event.data.object;
    await db.orders.markPaid(intent.metadata.order_id, intent.id);
  }
  if (event.type === "payment_intent.payment_failed") {
    const intent = event.data.object;
    await db.orders.markFailed(intent.metadata.order_id);
  }
  res.sendStatus(200);
});

app.post("/api/checkout", express.json(), async (req, res) => {
  const order = await db.orders.get(req.body.orderId);
  const intent = await stripe.paymentIntents.create(
    {
      amount: order.amount_cents,
      currency: order.currency,
      automatic_payment_methods: { enabled: true },
      metadata: { order_id: order.id },
    },
    { idempotencyKey: `order_${order.id}` }
  );
  await db.orders.setPending(order.id, intent.id);
  res.json({ clientSecret: intent.client_secret });
});

Register the webhook route before any global express.json() middleware. Signature verification needs the raw bytes, and a parsed body will fail it every time. Amounts are in the smallest currency unit, so 4900 means $49.00.

Exposing checkout to an AI assistant

A SaaS team often wants a customer to say "upgrade me to the team pkan" inside Claude, ChatGPT or Cursor. The assistant should never see card details, and it should never set a price. The clean pattern is two tools that call your existing backend.

create_checkout takes a plan ID and a customer reference. Your server creates the order, creates the intent, and returns a link to your hosted checkout page, where the Payment Element runs as usual. get_payment_status takes an order ID and reads the status your webhook already wrote.

{
  "tool": "create_checkout",
  "input": { "plan_id": "team_monthly", "seats": 5 },
  "output": {
    "order_id": "ord_8421",
    "checkout_url": "https://app.example.com/pay/ord_8421",
    "status": "pending",
    "expires_in_minutes": 30
  }
}

A few rules we apply when we build this kind of server on client projects:

  • The tool schema accepts plan IDs from an enum, never a raw amount.
  • The tool authenticates as the user (OAuth or a scoped token), so orders attach to the right account.
  • get_payment_status reads your database, not the Stripe API, so it stays fast and consistent with the webhook.
  • Every tool call is logged with the order ID, so support can trace what the assistant did.
  • Tests cover the unhappy paths: expired order, declined card, duplicate call, and a webhook that arrives before the tool returns.

If you would rather not own that layer, this is the work our custom MCP server build covers: auth, tests, docs, hosting and registry listing around your API.

See the payment guide

Where a form builder fits

The payment step itself belongs in your code, since that is where Stripe's confirmation and your order state live. A form builder helps with what happens before it: collecting company name, seats, billing email, a tax ID or a purchase order number, then handing that to your backend.

Qwenvo is a free form builder our team builds. For this flow, the useful pieces are hidden fields (to carry a plan ID or UTM values from the URL), conditional logic (to show a PO field only for annual invoices), and a webhook to any URL, which can call your /api/orders endpoint to create the draft order. Webhooks, the API, an MCP server, custom domains, removing the badge and teammates are all free there. Check the payload format in the webhooks help page before wiring your handler.

One honest limit: Qwenvo's Payment block uses PayPal checkout, and it keeps a 1% platform fee on top of PayPal's own fee. It does not offer Stripe right now. If you want Stripe inside a no-code form, Tally adds no fee on top of Stripe, so it is the cheaper pick for taht. For the flow in this post, you would use Qwenvo for intake only and kep payment in your own code.

Build your intake form free

What to measure after launch

Track the gap between orders created and orders paid, split by payment method, since a sudden drop usually points at a broken confirmation step or a webhook that stopped arriving. Alert on webhook responses that are not 2xx, and on orders that sit in pending longer than your expiry window. For the assistant path, count create_checkout calls against completed payments, so you can tell whether people finish what the assistant starts.

Questions teams ask

Can I confirm the payment from the frontend only?

You can call confirmPayment from the browser, but you should not mark an order paid there. Confirmation can succeed while the customer closes the tab before your page loads, and a redirect URL can be visited by anyone. Update order state only from the signed webhook.

Why does my webhook signature check keep failing?

Almost always the body was parsed before verification. Use the raw request body for constructEvent, make sure you are using the signing secret for that specific endpoint (test and live differ), and check that no proxy rewrites the body.

Should the assistant create the PaymentIntent directly?

Have it call a tool that calls your backend. Your backend owns pricing, idempotency and order state. If the assistant held a Stripe secret key or chose amounts, you would have no reliable place to enforce rules.

Do I need Qwenvo to follow this example?

No. The payment flow works without any form builder. Qwenvo helps wehn you want a quick intake step in front of it, with hidden fields and a webhook, without building that form UI yourself.

Next step

Write down the three or four actions you want an assistant to perform on your product, such as start checkout, check payment status and cancel a subscription. Map each to an existing endpoint and its auth rule. That list is the scope of a first MCP server, and it is the best thing to bring to a scoping call.

This system

Custom MCP Server

$3,000 setup
+ $349/month care plan

Third-party platform and model usage billed to you directly. No lock-in. Live in 10 days.

More on Custom MCP Server