Stripe Integration Development
Stripe integration development, built for the failure modes rather than the happy path.
You’ve already chosen Stripe. What you need next is somebody who has cleaned up after at-least-once webhook delivery, not a marketplace renting you a body for forty hours. This page covers the decisions that determine whether a Stripe build is still correct six months in. How duplicate events get absorbed, where idempotency keys come from, what reconciliation compares against, and who inherits dispute liability under each Connect account type. We build with Stripe. We’re not a Stripe partner, and no badge here will pretend otherwise.
If you’re still settling the stack around the payments layer, How we build web applications covers the framework and hosting decisions this page assumes are made. If the real question is regulatory rather than technical, our fintech and payments engineering page works through PCI scope and the SAQ forks.
Engineering position
Six positions a body shop won’t take for you.
At-least-once delivery and double-billing are the same sentence
Stripe will send you the same event twice, on a schedule you don’t control. A retry after your endpoint timed out, a redelivery after a connection reset, a manual resend somebody triggered while debugging, and in live mode those retries keep coming with backoff for days. A handler that grants a month of access on invoice.payment_succeeded grants two months the first time it happens, and nobody notices until the customer does. The absorbing layer is a received-events table with a unique constraint on the Stripe event id, an insert attempted before any side effect, and the constraint violation itself treated as the deduplication. Ordering has the same shape. Events are not guaranteed to arrive in the order they occurred, so a handler that trusts arrival order will resurrect a plan cancelled ninety seconds earlier.
Receive fast, process safe: verify, persist, return 200, then work
Signature verification runs over the exact bytes Stripe sent, so any framework that parses JSON first has already destroyed the payload. In the App Router that means req.text() and constructEvent on the raw string, never the parsed object round-tripped back through JSON.stringify. The Stripe-Signature header carries a timestamp with a five-minute default tolerance, so clock drift on your host shows up as signature failures that look exactly like a wrong secret. Then persist the raw event and return 200 straight away. Stripe treats a slow endpoint as a failing one, so three inline API calls turn a single slow dependency into a retry storm aimed at the endpoint that’s already struggling to answer.
Idempotency has two layers and most integrations ship one
Deduplicating the event id protects the inbound side. The outbound side needs its own guard, because your service retries too. Every POST to the Stripe API accepts an Idempotency-Key header, and Stripe caches the first response against that key for 24 hours instead of creating a second PaymentIntent or a second refund. Where the key comes from is what decides whether any of that works. Use a stable domain identifier such as the order id, or an explicit attempt number, and never a fresh UUID generated inside the retry, because a new UUID is just a new request wearing the header.
The webhook is a notification. The Stripe API is the ledger.
Any integration that’s run for a year has missed an event. The endpoint got disabled after repeated failures, a deploy window swallowed ten minutes, somebody rotated a secret on a Friday. If webhook handlers are the only thing that ever writes to your database, those gaps are permanent and silent. There’s no error anywhere, just a customer who paid and didn’t get the thing. A reconciliation job closes them. List charges, invoices and subscriptions over a moving created window, compare each one against your own row, and report the discrepancy before you correct it. Pin the API version too, because webhook payloads are serialised at the version fixed on the endpoint.
Connect: account type is a liability decision, charge type is a money-movement one
Standard, Express and Custom differ far less in code than in who’s holding the bag. On Standard the seller holds a full Stripe account, completes Stripe’s own identity verification, gets the dashboard, and carries dispute and negative-balance liability themselves. On Express, Stripe still hosts onboarding, but the platform inherits support and, in practice, the losses when a seller’s balance goes negative. On Custom the platform owns onboarding, KYC collection, the dashboard, support and every chargeback. The current API states all of this through controller properties rather than one type field. Choose by who takes the call when a chargeback lands.
The charge topology follows from that. A destination charge puts the payment on the platform and moves funds in the same call with transfer_data.destination, while on_behalf_of makes the connected account the settlement merchant, which changes the statement descriptor and the country the payment settles in. Separate charges and transfers split those steps. That’s the only workable shape when one payment fans out to several sellers, or when the split isn’t known at charge time, and it costs you the job of managing available balance yourself. Platform revenue is application_fee_amount or application_fee_percent, and that number is what your unit economics rest on.
Authentication, off-session retries, and keeping the card number off your servers
European cards require strong customer authentication, which the networks satisfy with EMV 3-D Secure 2. So you need PaymentIntents rather than direct charges, and you have to decide at the first payment whether you’ll ever charge this customer again. Either setup_future_usage or a separate SetupIntent captures the mandate while they’re still present. Get it wrong and the first off-session renewal comes back requiring authentication, the charge doesn’t settle, and the only remedy is bringing a human back to a browser. That recovery flow is the one that gets deferred past launch and then written in a hurry during the first billing cycle.
Where card data lives gets decided once. Checkout and Elements both send the card number from the browser straight to Stripe, so it never touches your origin, and that’s the largest single reduction in PCI scope available to you. That does not take scope to zero. v4.0.1 still hands the embedding page requirements 6.4.3 and 11.6.1, meaning an authorised inventory of every script on the payment page plus tamper detection, both mandatory since 31 March 2025. Stripe Tax computes the rate per line item, and it will not tell you where you have registration obligations.
The webhook half is worked out end to end, with code, in Stripe webhooks in production. If your ledger and your entitlements live in Postgres, the other side of the same write path is how we build on Supabase.
How it runs
What we guarantee, and where a payments build gets expensive.
Your repo and your cloud from commit one
Handlers, migrations, the reconciliation job and the event fixtures live in your repository from the first commit, under your Stripe account rather than ours.
A staging URL by the end of week one
Deployed and clickable against Stripe test mode, with the CLI forwarding signed events at it. A build and a written note every Friday after that.
Decisions in writing
The Connect account type, the charge topology, the pinned API version and where every idempotency key comes from, recorded with the reasoning attached.
Fixed scope, quoted before kickoff
Change requests are priced and confirmed in writing before anyone starts. Payments work has a tail of small discoveries, and that tail gets quoted rather than absorbed.
We quote fixed scope against a published rate card instead of selling hours, and that’s the real difference between this page and the staffing marketplaces sitting above it in the results. Platforms start at $28,000 and most projects land between $18,000 and $45,000. The minimum for a build is $7,500, and ongoing work after launch starts at $2,500 a month. Those published figures are estimates in US dollars, not quotations; the quote that follows scoping is fixed in writing before kickoff.
What moves it is whether Connect is in scope, how many distinct charge shapes exist, whether subscriptions carry proration or metered usage, and whether we’re repairing an integration or writing one. The same reasoning sits behind what an MVP actually costs. Send the pricing model and say who gets paid out of each payment, and you’ll get the charge topology back before you get a price.
Send the pricing model. Get the charge topology back first.
Tell us who gets paid out of each payment and we’ll map it to charges, transfers and fees before we quote. Fixed scope, fixed price, start date within two working days. Email edgebrainstudios@gmail.com or call the number below.