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.

Six artefacts you keep after the integration ships.

Not a checkout button wired to a handler that assumes every event arrives exactly once. You’ll judge this build on the day Stripe resends an invoice event and nothing bad happens.

A webhook endpoint that survives redelivery

Signature verified against the raw bytes, the event id written under a unique constraint before any work starts, and a 200 returned before provisioning runs.

Idempotency on both sides of the wire

Event-id deduplication on the way in, an Idempotency-Key derived from your order id on the way out. With only one layer, the retry that saves you also bills twice.

A reconciliation job you can run on demand

A sweep that lists charges, invoices and subscriptions from the Stripe API over a moving window and diffs them against your rows before correcting anything.

A written Connect decision

Which account type, who completes identity verification, who is liable for disputes and negative balances, and whether money moves as a destination charge or a transfer.

A subscription state machine, not a boolean

trialing, active, past_due, unpaid and canceled mapped to what the product unlocks, including the grace period nobody specifies until the first failed renewal.

Tests that replay real signed events

The Stripe CLI firing signed fixtures at your endpoint in CI, including the duplicate and the out-of-order case. Neither one is reproducible by clicking around a dashboard.

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.

When not to hire us for it

Five payment problems we wouldn’t solve with Stripe.

Start with the first one. A large share of the people searching for a Stripe developer don’t need to hire anybody at all.

One product, one price, no marketplace

Payment Links and hosted Checkout do it in an afternoon, and Stripe’s own documentation beats any agency page, this one included. Hiring anybody for it wastes your money.

Your category sits on the restricted list

Stripe publishes what it will not support, and finding out after the build is expensive. A high-risk acquirer is a different integration, not a config change.

You move enough volume to negotiate interchange

Blended pricing is a convenience with a price attached. Past serious annual volume, direct acquiring through a gateway such as Adyen outranks developer experience.

Enterprise billing with ramp deals and revenue recognition

Stripe Billing is strong at subscriptions and weak at negotiated contracts, mid-term ramps and ASC 606 recognition. A dedicated billing platform fights you less.

You need to hold or lend money rather than route it

Escrow held for weeks, lending, or anything where you hold customer funds is a licensing question before it’s an API question. Talk to a lawyer first.

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.

FAQ

The questions that decide it

Delivery is at-least-once, so the same event turns up twice sooner or later. We write the Stripe event id into a table with a unique constraint and attempt that insert before any work runs, so a duplicate fails the constraint and does nothing at all. Outbound writes carry an Idempotency-Key derived from your order id, never a UUID regenerated inside the retry.

Decide by liability rather than by integration effort. Standard gives the seller their own Stripe account, dashboard and dispute exposure. Express keeps Stripe-hosted onboarding but pushes support and negative-balance losses onto the platform. Custom means you own onboarding, KYC collection, support and every chargeback. The current API states all of this through controller properties rather than one type field.

Checkout is a hosted page Stripe maintains for you, including the new payment methods and authentication behaviour you’d otherwise be chasing yourself. Elements embeds fields in your own interface while the card number still goes straight to Stripe. Use Elements when checkout is part of the product experience, and Checkout when it’s a step toward one.

Frequently, and the first pass is read-only. We replay a week of events at a staging endpoint, run a reconciliation sweep to find where your database and Stripe disagree, and list every write path that has no idempotency key. That report is the quote, and the repairs usually land behind the endpoint that’s already there.

An off-session charge on a European card can come back requiring authentication, and no retry schedule fixes that. The customer has to complete 3D Secure in a browser. So the recovery path is part of the build, which means the email, the hosted link, the grace period, and a map from Stripe statuses to what the product unlocks.

We quote fixed scope before kickoff rather than selling hours. Platforms start at $28,000 and most projects land between $18,000 and $45,000, with a minimum of $7,500 for a build. Those figures are estimates in US dollars, not quotations; the quote that follows scoping is fixed in writing before kickoff. What moves the number is whether Connect is involved, how many charge shapes exist, and whether we’re repairing an integration or writing one from scratch.

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.