Milo Solutions

Payment API integration: a developer's guide

Introduction

Payment API integration means connecting your application to a payment provider so it can authorize, capture, refund, and reconcile money. Getting the first charge to succeed is the quick part, and the reliability comes from how you handle everything that follows: duplicate requests, missed webhooks, declines, and the gap between what your database believes happened and what actually settled.

I run Milo Solutions, and my team has built payment flows from scratch and been called in to fix ones that were quietly losing money, and that money rarely drains away at the checkout screen itself. Card fraud losses worldwide reached $33.41 billion in 2024, with the United States accounting for 41.87% of those losses despite generating just 26.31% of global card volume, according to the Nilson Report [2]. Almost all of the US total is card-not-present fraud, the kind that happens on exactly the sort of online payment you're about to build, so most of the risk and most of the engineering sit in your code rather than on the provider's hosted page.

Key takeaways

  • Your integration pattern, whether a hosted page, embedded fields, or a direct API, decides how much of the PCI Data Security Standard lands on you, so pick it deliberately.
  • Build idempotency and webhook signature verification on day one, because both are painful to retrofit after an incident.
  • Treat security as part of the build, since PCI DSS v4.0.1 now puts the scripts on your payment page in scope.
  • Reconciliation, which checks your own records against the provider's settlement data, is how you find out whether the integration actually works.

What does a payment API actually do in your stack?

A payment API is an interface your backend calls to move money: you authenticate with a secret key, send a request describing the payment (the amount, the currency, and a token that stands in for the customer's card), and read a response containing a transaction ID and a status, and that cycle of request, response, and status is the whole loop you work with.

The API sits between your application and the rest of the card-processing chain, the payment processor, the card networks such as Visa and Mastercard, and the customer's issuing bank. Your code never talks to the bank directly but to the provider, which handles the underlying machinery.

One distinction that the search results keep blurring is between a payment API and a payment gateway: the gateway is the service that securely passes card data to the processor, while the API is the programmable interface you use to drive it, creating charges, issuing refunds, and reading transaction status. Modern providers like Stripe, Adyen, and PayPal offer both under a single account, so what matters more is which parts of that chain touch your servers, as that determines your compliance scope.

How do you choose an integration pattern, and why does it set your PCI scope?

The pattern you pick before writing any integration code is the decision that follows you for the life of the product, because it sets how much card data ever touches your servers, and therefore how much of the PCI Data Security Standard you are responsible for.

At Milo, we start with the business model rather than the most customizable option, because speed to market, how much control you need over the checkout, where you sell, and whether you also take in-person payments all pull the decision in different directions. The common mistake I see is teams treating customization as maturity, reaching for full API integration and taking on scope and complexity the product does not yet need.

Hosted payment page

The provider hosts the payment page and redirects your customer to it to enter card details, so card data never touches your servers, and you sit in the lightest compliance tier, the PCI DSS Self-Assessment Questionnaire A. In exchange, you give up control because the page belongs to the provider, and you get less say over how checkout looks and behaves, which is often an acceptable trade when speed to market matters more than a pixel-perfect flow.

Embedded fields (SDK)

Tools like Stripe Elements and Braintree Hosted Fields drop the card inputs into your own page inside an iframe the provider controls, so the customer stays in your app, the checkout looks like yours, and the card details still go straight to the provider without hitting your server. You stay eligible for the lighter SAQ A tier, but with a catch that PCI DSS v4.0.1 made explicit, because you now own responsibility for the scripts running on that page, which the section on PCI DSS v4.0.1 works through in detail.

Direct server-to-server API

Your backend receives raw card data and sends it directly to the provider, which gives you the most control and the most burden, because your environment then falls into the heaviest PCI scope, SAQ D, with the full set of requirements that implies. We reserve this pattern for the cases that genuinely need it, and provider choice matters just as much here, especially for international sales or physical card terminals.

Pattern Card data touches your servers? Typical PCI SAQ Control over UX Best fit
Hosted payment page No SAQ A Low Fastest route to a compliant checkout
Embedded fields (SDK) No (provider iframe) SAQ A Medium to high Branded in-app checkout, lighter scope
Direct server-to-server API Yes SAQ D Full Complex flows that need full control

For most products, I steer clients to embedded fields because you maintain a consistent in-app experience, the provider handles sensitive card handling, and you stay in the lighter compliance tier. Hosted pages make more sense when speed matters more than anything else, and direct API integrations earn their place only when the product genuinely needs that level of control.

How do you build a payment integration, from credentials to go-live?

The path is roughly the same across providers, and we work through it in a consistent order.

  1. Get sandbox and production credentials. Every provider gives you a test environment with fake card numbers, and you will live in the sandbox for most of the build.
  2. Build the sandbox flow first, wiring up the core endpoints before you touch the frontend.
  3. Implement three endpoints: a charge (or payment intent), a refund, and a webhook handler, and give the webhook handler real attention because it is the one people rush.
  4. Embed the payment form with the provider's frontend SDK, so card details go straight to the provider.
  5. Run user acceptance testing on the provider's test cards, which include specific numbers that trigger a decline, an insufficient funds error, or a 3D Secure authentication challenge, and exercise all of them.
  6. Switch to production and watch the first 48 hours closely: webhook delivery, authorization rates, and any error spikes.

A minimal charge request and its response look something like this, simplified and not tied to any one provider:

POST /v1/payments
Authorization: Bearer sk_live_...
Idempotency-Key: order_8842_attempt_1

{
"amount": 4200,
"currency": "usd",
"payment_method": "pm_card_token",
"capture": "manual"
}

// response
{
"id": "pay_3Nk9",
"status": "requires_capture",
"amount": 4200,
"currency": "usd"
}

Note that the amount is in minor units, so 4200 means $42.00, and that Idempotency-Key header does more work than it looks, since it is one of the details that separate a working demo from a payment system you can trust.

Which parts of a payment integration do teams underestimate?

The integrations that come to us already broken are almost always built for the happy path, where payments succeed in testing, and the demo works, and then production finds the gaps, four of which come up again and again.

Idempotency

A network blip between your server and the provider is common enough that you have to design for it, because when your request goes out and the connection drops before the response comes back, your code cannot tell whether the charge went through, so it retries, and without protection that retry becomes a second, duplicate charge.

You solve that with an idempotency key, attaching a unique key to the request so the provider guarantees that repeating the same key returns the original result instead of creating a second charge. Stripe stores the result of the first request and replays it for any retry that carries the same key [3], so generate the key once per payment attempt rather than per HTTP call and store it with the order. Build this on day one, because retrofitting it after a double-charge incident costs far more than adding a header.

Webhooks are where the truth arrives

A payment completes only when the provider confirms it, and that confirmation reaches you by webhook, an HTTP POST to an endpoint you expose, so treating the customer's click as the finish line is what causes orders and payments to drift apart, and three practices keep that from happening.

Start by verifying the signature, because providers sign each webhook so you can confirm it genuinely came from them and was not forged, and Stripe signs every event and expects you to verify the signature before trusting the payload [4]. Skip that step, and anyone who finds your endpoint can tell your app that a payment succeeded.

Make the handler idempotent as well, because webhooks get redelivered, sometimes more than once, so processing the same event twice must not create two orders or two refunds. Return a 2xx response quickly and do the heavy work of updating records and sending email in a background job, since a slow handler appears to the provider as a failure, which then retries and compounds the problem.

How to handle declines without making them worse

A decline can mean two very different things, and telling them apart is what stops you from making the problem worse. Soft declines are temporary, covering things like insufficient funds, an issuer's fraud check, or a timeout, while hard declines are final, such as a closed account, a stolen-card flag, or an invalid number, and the two call for opposite responses.

A soft decline can often be recovered with a retry on exponential backoff, waiting a little longer between attempts, whereas retrying a hard decline is pointless, and repeatedly retrying cards you know are bad can get your account flagged by the issuer. Read the decline code the provider returns and branch on it, so a recoverable payment and a genuine fraud signal are never handled the same way.

Reconciliation

The failure that hides longest is reconciliation drift, where your webhook said the payment succeeded, your database marked the order paid, and everyone moved on, until weeks later finance notices that the money never settled, or that it settled for a different amount. Your internal state and the provider's records had drifted apart, and nobody was watching the gap.

Reconciliation is what closes that gap, comparing your own records with the provider's settlement data regularly and flagging any discrepancies. A payment system you can trust is always able to establish what actually happened after a missed event or an uncertain response and then recover, whether by retrying, querying the provider directly, or reconciling on a schedule, and almost no competing guide takes this seriously, which is the difference between a checkout that demos well and one you can run a business on.

What does PCI DSS v4.0.1 mean for the person writing the code?

PCI DSS is the security standard that every business handling card data must meet, and its current release, version 4.0.1 from the PCI Security Standards Council, changed two requirements that now fall on front-end developers, both of which are mandatory as of 31 March 2025 [1].

What shrinks your obligations is tokenization, where the provider replaces the card number with a token so raw card data never reaches your systems, which is exactly what hosted pages and embedded fields do for you and why they keep you in the lighter SAQ A tier.

Version 4.0.1 closed a gap that catches teams out, though, because even when card data goes straight to the provider, the scripts on your payment page can be tampered with to skim details before they are tokenized, an attack known as digital skimming, so two requirements now apply to your own page:

  • Requirement 6.4.3 requires you to manage every script on your payment page, which means maintaining an inventory, recording a written justification for each, and confirming that each is authorized and has not been altered [1].
  • Requirement 11.6.1 says you must run a mechanism that detects unauthorized changes to those scripts and to the page's security-relevant HTTP headers and alerts your team at least weekly [1].

The ownership split is the part worth getting right, because you own the scripts and headers on your own page, outside the provider's iframe, while the provider owns what happens inside it. Reaching for a full direct-API integration does not escape any of this, and it only piles the far heavier SAQ D scope on top.

Requirement What it asks for Who owns it
6.4.3 Inventory, justify, authorize, and integrity-check every script on the payment page Merchant (you)
11.6.1 Detect unauthorized changes to payment-page scripts and security headers, and alert Merchant (you)
Card capture inside the iframe Secure handling of the entered card data Payment provider

When does one provider stop being enough?

For most products, a single provider is plenty, and you add a second only when there is a concrete reason, whether declines you want to retry on a different route, regional coverage a single provider cannot give you, or the risk of a provider outage taking your checkout down with it. At that point, teams either integrate a second provider directly or move to a payment orchestration layer that routes transactions across several.

Adding a provider is always a real trade-off, because you gain another route for retries or another region while taking on a second integration to maintain, test, and reconcile, and orchestration gives you configurable routing but adds a dependency of its own, so we add one only when the business reason is clear.

How we approach payment integrations at Milo

When a client comes to us with payments, we do not start from a preferred architecture but from how the application makes money, where it operates, what systems the business already runs, and how invoicing and accounting need to work, and only then do we choose the provider and the integration that goes with it. We also push past testing only the payments that succeed, because failures and stale states have to be visible and recoverable, so the solution fits the whole business rather than the checkout screen alone.

That approach played out on a recent project where we built the checkout and the supporting payment workflows for an online therapy marketplace whose verified specialists offer individual sessions and multi-session packages, so a customer can find a therapist, choose a session or a package, and pay without leaving the app.

We used Stripe Payment Intents with Stripe Elements, embedded in a Django and Django Oscar application backed by PostgreSQL, with Celery and Redis handling scheduled and asynchronous work, and we chose Elements to keep a consistent in-app booking experience while Stripe handled the sensitive card details.

The hard part was keeping the payment lifecycle in sync with the booking workflow, because the integration relied on manual capture and had to handle basket changes, shifting session availability, abandoned checkouts, failed payments, and authorizations that were never captured. We built recovery for stale payments, cancellation and refund handling, and safeguards to ensure a session was created only once the payment and order states were valid.

We also built an internal settlement workflow for the therapists, where the platform calculates what each specialist is owed based on completed sessions, payment cycles, commission tiers, and VAT, and operations staff then prepare invoices, make the transfers, and record completed payouts through the back office.

The result was an end-to-end flow that connects Stripe payments to session availability, orders, packages, refunds, recovery, and therapist settlement, validated with manual end-to-end testing and a full suite of automated unit and Django integration tests.

The stack we reach for around payments stays consistent: Python and Django with Django REST Framework, PostgreSQL, and Celery with Redis or SQS for background jobs, with signature-verified and idempotent webhook processing, Sentry and structured logs for monitoring, and pytest with mocked webhooks and sandbox environments for testing, all shipped with Docker and CI/CD on AWS.

Across projects we have integrated Stripe, Adyen, PayU, PayPal, Moyasar, and HyperPay, so if you are weighing up a payment build, or trying to rescue one that is misbehaving, that is the kind of work we do.

Frequently asked questions

How long does payment API integration take?

A straightforward integration with a single provider and a hosted or embedded checkout can get a working sandbox flow up and running in a couple of weeks, while production-ready work that covers idempotency, webhooks, declines, reconciliation, and testing usually takes longer. Timelines vary with scope, so treat any figure as a starting estimate rather than a quote.

Do I need to be PCI compliant to integrate a payment API?

Yes, any business that handles card data has PCI DSS obligations. Using a hosted page or embedded fields keeps card data off your servers and puts you in the lighter SAQ A tier, but under PCI DSS v4.0.1 you are still responsible for the scripts on your payment page, and a direct API integration puts you in the heavier SAQ D scope.

What's the difference between a payment API and a payment gateway?

A payment gateway is the service that securely passes card data to the processor, while a payment API is the programmable interface your code uses to create charges, issue refunds, and read transaction status. Most modern providers give you both under a single account, so the distinction matters less in day-to-day work than it once did.

Which integration method should I use?

For most products, embedded fields such as Stripe Elements give you a branded in-app checkout while keeping card data off your servers and maintaining SAQ A compliance. Choose a hosted page when speed matters more than control, and a direct API integration only when you genuinely need the control and can carry the added PCI scope.

How do you handle webhooks securely?

Verify the signature on every webhook before you trust it, so a forged request cannot falsely tell your app that a payment succeeded, and make the handler idempotent because providers redeliver events. Return a 2xx response quickly and do the heavy work in a background job, so slow processing does not trigger unnecessary retries.

Reference Sources

Disclaimer: this is technical guidance for developers and technical leads, not security or compliance certification advice. Cost and timeline figures are general estimates and vary by scope. Confirm your own PCI DSS obligations with a Qualified Security Assessor for your specific setup.