DEV Community

Jessica Bennett
Jessica Bennett

Posted on

How to Build a Secure Payment Gateway Integration: A Developer’s Guide

A secure payment gateway

Connecting a payment gateway to an application can look deceptively simple.
Create an order, send the customer to checkout, wait for a success response, and mark the order as paid.
In production, however, every one of those steps introduces a trust decision.
Can the amount coming from the browser be modified? What happens if the same webhook arrives twice? Should an order be fulfilled because the customer reached a success page? What happens when the gateway says a payment succeeded, but your database update fails?
A secure payment gateway integration is therefore less about making an API request work and more about making sure your application knows which information it can trust before money triggers a business action.
This guide walks through the main security decisions developers should make when building a payment integration.

How a Secure Payment Gateway Integration Should Work

A simplified payment flow might look like this:
Customer

Merchant Frontend

Merchant Backend

Payment Gateway

Bank / Payment Network

Payment Gateway

Webhook

Merchant Backend

Order Updated

The important boundary here is between the frontend and the backend.
The frontend is useful for:

  • displaying checkout
  • collecting customer choices
  • starting the payment flow
  • showing the customer the current state

The backend should be responsible for:

  • calculating the amount payable
  • creating the internal order
  • communicating with payment APIs
  • protecting secret credentials
  • verifying payment status
  • rocessing webhook events
  • Updating the final order state

The basic rule is simple:
Treat the browser as an interface, not as the source of truth for payment decisions.

Anything sent from the browser can potentially be inspected or modified.

Choose the Right Integration Model First

Before writing payment code, understand how payment information will move through your application.

Hosted Payment Page

In a hosted model, customers are redirected to a checkout page controlled by the payment provider.

The provider handles much of the sensitive payment data collection, while your application usually sends the transaction information required to create the payment.

This can reduce the amount of payment data that passes through your own systems.

Embedded or Hosted Payment Fields

Some providers allow payment fields to appear inside the merchant's checkout while the sensitive components themselves are delivered by the payment provider.

This gives the merchant more control over the user experience without necessarily collecting every sensitive field directly.

Direct API Integration

A direct integration can offer greater control over the checkout and payment flow, but it may also introduce additional security and compliance responsibilities.

The important point is that PCI DSS scope depends partly on the architecture you choose.

A merchant using a fully hosted payment page may have different responsibilities from one whose systems directly participate in the collection or handling of cardholder data.

Do not assume that adding a compliant payment provider automatically makes the rest of your application compliant.

1. Calculate Prices on the Backend

One of the easiest mistakes to make is trusting the amount sent by the frontend.

Suppose the browser submits:
{
"productId": "PHONE_108",
"quantity": 1,
"amount": 49999
}

It is tempting to pass 49999 directly to the payment gateway.
Don't.

A user controls their browser. Requests can be changed with browser developer tools, intercepting proxies, scripts, or direct API calls.
Instead, let the browser tell the backend what the customer wants to purchase.

Then calculate the amount again using trusted data:
const product = await products.findById(productId);

const subtotal = product.price * quantity;
const discount = calculateDiscount(product, user);
const tax = calculateTax(subtotal - discount);

const payableAmount = subtotal - discount + tax;

The server should determine:

  • product price
  • quantity rules
  • discounts
  • taxes
  • delivery charges
  • final amount

Otherwise, a malicious request could potentially change:
₹49,999

to:
₹499

While still referring to the same product.

OWASP's guidance for third-party payment integrations recommends validating prices, product information, quantities, discounts, and totals using trusted server-side data.

2. Keep Secret Credentials Away From the Client

Payment APIs usually require credentials.
Some credentials may be designed for client-side use, depending on the provider. Secret API credentials are different.
They should not appear in:

  • frontend JavaScript
  • public repositories
  • browser storage
  • URL parameters
  • client-visible API responses
  • committed .env files
  • mobile application code where a true server secret is required A safer architecture is:

Browser

Merchant Backend

Payment Gateway

The merchant backend authenticates itself to the payment provider.

Use environment-specific secret storage or an appropriate secrets-management service, and rotate credentials according to your organisation's security practices.

Also, make sure production and test credentials are kept separate.
A surprising number of payment-security problems start with a credential being treated as ordinary configuration rather than as a secret.

3. Use HTTPS, but Don't Mistake It for Complete Security

All payment-related communication should use HTTPS.

That includes communication between:
Browser ↔ Merchant

Merchant ↔ Payment Provider

TLS protects data while it travels between systems.
But HTTPS will not protect your application from bad business logic.
It cannot prevent:

  • accepting a manipulated amount
  • leaking an API key
  • processing an unauthenticated webhook
  • fulfilling an order twice
  • trusting a forged payment status
  • broken access control inside your application

Transport security is one layer of payment security, not the whole design.

4. Give Every Order a Reliable Identifier

Your application should create its own internal order reference before the payment process begins.
For example:
Merchant Order ID: ORD_84721
Payment ID: PAY_72918

Store the relationship between them.
This allows you to trace:
ORD_84721

PAY_72918

A reliable mapping helps with:

  • payment verification
  • reconciliation
  • refunds
  • support queries
  • failed-payment investigation
  • duplicate-event detection

Avoid identifying transactions only by values such as email address, amount, or timestamp.

Those are useful metadata, but they are poor substitutes for unique transaction references.

5. Don't Treat the Success Page as Proof of Payment

Consider this redirect:
https://example.com/order-success?order=84721

The customer reaches that URL after checkout.
Does that prove Order 84721 was paid?
No.
A customer might:

  • Reload the URL
  • bookmark it
  • manually open it
  • change query parameters

reach it even when the browser flow is inconsistent with the final backend state

The success page should answer:

What should the customer see?
It should not independently answer:
Has the merchant definitely received a valid, successful payment?
A better model is:
Redirect

Customer experience

Server-side payment status

Business decision

Your application can display a processing state while the backend confirms the actual payment result.

Verify the Payment Before Fulfilment

Before shipping a product, issuing account credits, activating a subscription, or performing another irreversible action, verify the payment against trusted information.

Depending on the gateway, that can include checking:

  • payment ID
  • merchant order ID
  • payment status
  • amount
  • currency
  • merchant reference Suppose your database expects: { "orderId": "ORD_84721", "amount": 5000, "currency": "INR" }

But the payment record contains:
{
"orderId": "ORD_84721",
"amount": 500,
"currency": "INR",
"status": "SUCCESS"
}

The payment may have succeeded.

The order still should not be fulfilled.

Your backend expected ₹5,000 and received confirmation of ₹500.
A simplified verification could look like:
const validPayment =
payment.status === "SUCCESS" &&
payment.orderId === order.id &&
payment.amount === order.amount &&
payment.currency === order.currency;

if (validPayment) {
await fulfil(order);
}

Real implementations should follow the gateway's API and security requirements, but the principle stays the same:

Verify the transaction you expected, not merely the existence of a successful payment.

Secure Payment Webhooks

Webhooks allow the payment provider to notify your backend when something changes.
For example:
Payment Gateway

payment.success

POST /api/payments/webhook

Merchant Backend

They are especially useful because payment confirmation does not depend entirely on the customer's browser remaining open.

But a webhook endpoint is exposed to the internet.

Receiving an HTTP request at /api/payments/webhook does not prove that the gateway sent it.

Verify the Webhook Signature

Payment providers typically document a mechanism for authenticating webhook requests.
Depending on the implementation, this may involve a cryptographic signature, HMAC, shared secret, or another verification method.
Conceptually:
const verified = verifySignature(
rawBody,
requestSignature,
webhookSecret
);

if (!verified) {
return res.status(401).send("Invalid webhook");
}

Use the exact verification method documented by the provider.

Do not create your own signature format when the gateway already defines one.

Compare the Event With Your Own Order

Webhook authentication proves that the request came from an expected source.

It does not mean you should immediately perform fulfilment.
Compare fields such as:
order ID
payment ID
amount
currency
status

With the order stored in your database.

Authentication and transaction validation solve different problems. You need both.

Make Webhook Handling Idempotent

Assume the same event can arrive more than once.
A retry could produce:
Webhook 1 → mark order paid → ship

Webhook 2 → ship again

That is obviously a problem.
Instead, your application should behave like this:
Webhook 1 → process

Webhook 2 → event already processed → no duplicate action

This is idempotency.
A simple implementation might record processed event IDs.
For example:
CREATE UNIQUE INDEX unique_payment_event
ON processed_payment_events(event_id);

When an event arrives:
Start transaction

Insert event ID

Already exists?
↙ ↘
Yes No
Stop Update payment
Trigger fulfilment

Database constraints are useful here because they can also protect against two workers attempting to process the same event at almost the same time.
A boolean such as:
payment_processed = true

May be useful, but by itself it can still be vulnerable to race conditions if concurrent requests read the same state before either one updates it.

Protect Against Replay Attacks

A duplicate event may happen because the provider retried delivery.
A replay attack is different.
An attacker takes a valid message and intentionally submits it again.
For example:
Legitimate payment event

Captured

Replayed later

Duplicate fulfilment

Controls can include:

  • unique transaction or event IDs
  • webhook signatures
  • processed-event storage
  • timestamps
  • expiry windows
  • rejecting previously processed transactions

Idempotency reduces the damage from duplicates.

Replay protection focuses on preventing old valid messages from being reused maliciously.
Payment systems should account for both.

Treat Payment Status as a State Machine

A payment is rarely just:
true / false

Real transactions can move through multiple states.
A simplified example:
CREATED

PENDING

PAID

Other paths may include:
PENDING → FAILED

PENDING → CANCELLED

PAID → REFUNDED

Your provider will define its own states, but your application should preserve important distinctions.

This is particularly important for uncertain transactions.

If your application cannot yet determine whether a payment succeeded, don't silently convert that uncertainty into success.

Keep it pending until you have a reliable final status.

Your payment flow should also account for cases such as:

  • Customer closes the browser
  • Authentication times out
  • Payment remains pending
  • Webhook is delayed
  • Payment succeeds, but the order update fails
  • Refund begins after payment success

Explicit statements make these situations much easier to reason about than one paymentSuccessful boolean.

Protect the Payment Page From Third-Party Script Risk

Payment-page security isn't only a backend issue.

Modern checkout pages frequently load third-party JavaScript for:

  • analytics
  • tag managers
  • experimentation
  • customer-support widgets
  • advertising
  • fraud tools

Every script running on a payment page increases the number of components that need to be trusted.

A compromised script can potentially alter a checkout or capture information entered by a customer. This class of attack is often associated with e-skimming.

PCI DSS v4.x includes specific requirements around managing payment-page scripts and detecting unauthorized modification.

For developers, useful questions include:

  • Do we actually need this script on checkout?
  • Who controls it?
  • Can its content change without our deployment process?
  • Can we detect unexpected modifications?
  • Does it have access to sensitive page elements?

Removing unnecessary scripts from sensitive pages can sometimes be one of the simplest security improvements available.

Don't Log More Payment Data Than You Need

During integration work, this is tempting:
console.log(request.body);

It makes debugging easy.

It can also create a new security problem.

Application logs may later be copied into:

  • monitoring platforms
  • support systems
  • backups
  • issue trackers
  • developer machines

Log enough information to investigate a transaction without unnecessarily storing sensitive values.

Useful operational fields might include:
order_id
payment_id
event_id
payment_status
error_code
timestamp

Depending on your system.

Avoid putting sensitive data in URL query parameters as well. URLs may appear in browser history, server logs, proxy logs, analytics tools, and monitoring systems.

Test More Than the Happy Path

A successful sandbox transaction proves that your sandbox transaction works.

It does not prove the integration is production-ready.
Test scenarios such as:

Test Scenarios

Also test what happens after deployments and integration changes.
Payment failures often happen at the boundaries between systems, so those boundaries deserve deliberate testing.

Secure Payment Gateway Integration Checklist

Before moving to production, check that:

  • Prices and payable totals are calculated server-side
  • Client-supplied amounts are not trusted
  • Secret payment credentials remain on the backend
  • HTTPS is used for payment communication
  • Every internal order has a unique reference
  • Payment status is verified server-side
  • Browser redirects do not trigger fulfilment by themselves
  • Webhook authenticity is verified
  • Payment amount, currency, and order ID are checked
  • Duplicate webhook processing is safe
  • Replay attacks have been considered
  • Pending and failure states are modelled explicitly
  • Sensitive payment information is kept out of logs
  • Payment-page scripts are reviewed and monitored
  • Failure scenarios are tested
  • Production payment errors are monitored
  • Applicable PCI DSS requirements have been reviewed

A checklist cannot replace a security review, but it can catch many common integration mistakes before they reach production.

What Developers Should Look for in a Payment Gateway API

Security depends partly on the quality of the integration surface a payment provider gives developers.
Before choosing one, review more than the pricing page.

Documentation

Look for clear documentation covering:

  • authentication
  • creating orders
  • initiating payments
  • transaction status
  • refunds
  • webhooks
  • error responses
  • testing

Server-Side Payment Verification

The gateway should provide a reliable way for your backend to determine the actual transaction state.

Webhook Support

Check:

  • available events
  • authentication method
  • delivery behaviour
  • retry behaviour
  • event identifiers

Error Handling

Developers should be able to distinguish meaningful states such as:
customer error
issuer decline
authentication problem
merchant integration error
gateway error
pending transaction

Testing Environment

A sandbox should let developers test more than a perfect successful transaction.
Failure, pending, refund, and webhook scenarios matter too.

Fit With Your Architecture

A good API is one that fits your backend, frontend, security model, and operational requirements.
Developers evaluating a payment gateway should therefore compare API documentation, webhook support, transaction-status verification, testing capabilities, security requirements, and payment-method coverage alongside transaction pricing.

Secure Payment Integration Comes Down to Trust

The payment button may live in the frontend, but the final business decision belongs on trusted systems.
A safer architecture looks like:
Backend calculates the order

Gateway processes the payment

Backend verifies the transaction

Authenticated webhook updates state

Idempotent handler processes it once

Fulfilment

The principle behind all of these controls is straightforward:
Don't perform a business action simply because your application received payment-related data. Verify what the data says, where it came from, and whether the same event has already been processed.

That means calculating prices on the server, protecting credentials, verifying transaction details, authenticating webhooks, handling duplicate events safely, and designing for failure states from the beginning.

A secure payment gateway integration is not defined by one API call succeeding.

It is defined by what your application does when an assumption fails.

Frequently Asked Questions

How do you securely integrate a payment gateway?

Keep sensitive operations on the backend. Calculate prices server-side, protect secret API credentials, verify final payment status, authenticate webhooks, validate transaction details, make event handling idempotent, and test failure scenarios before production.

Should payment verification happen on the frontend or backend?

The backend should make the final payment decision. Frontend information can be used to update the customer experience, but fulfilment should depend on trusted server-side payment information.

What is webhook signature verification?

Webhook signature verification checks whether an incoming event genuinely came from the expected payment provider. The exact cryptographic method varies by provider and should be implemented according to its documentation.

Why is idempotency important in payment integrations?

Payment events may be delivered more than once. Idempotent processing makes sure duplicate delivery does not create duplicate shipments, subscriptions, account credits, or other business actions.

Does using a payment gateway make a website PCI DSS compliant?

No. Using a third-party payment provider can affect the merchant's PCI DSS scope, but compliance responsibilities depend on the integration architecture and applicable requirements.

Should an application trust the payment success redirect?

No. A success redirect is useful for the customer experience, but the backend should independently verify the payment before fulfilment or another irreversible business action.

Top comments (0)