Back to Guides
Payment Gateway API Integration: A Developer's Guide
Integrating a payment gateway is easy to do badly. Idempotency gets skipped, webhooks get processed inline, signatures get skipped on "trusted" networks, and six months later a production incident traces back to a duplicate charge or a lost callback.
This guide walks through the patterns that matter: API authentication, idempotency keys, signed webhooks, retry semantics, error handling, sandbox parity, and the specific extras for crypto payment gateway integration. Concrete code-level recommendations, not marketing bullets.
Boost Your Business by Accepting Crypto Payments
The Happy Path Is 5% of the Work
A first-pass integration looks simple: POST to create a charge, redirect the buyer, receive a webhook, mark the order paid. You can demo this in a weekend.
Production integration is different. 95% of the code you ship - and 100% of the code that causes incidents - handles:
- Duplicate requests (network retries, browser back button, double-click).
- Duplicate webhook deliveries (at-least-once semantics).
- Lost webhooks (firewall, timeout, deploy-in-progress).
- Partial failures (charge succeeded, but our DB write failed).
- Race conditions between API response and webhook.
- Refunds, disputes, and late state transitions.
- Sandbox-vs-production drift when an edge case fires only in prod.
Build for that world from day one.
Authentication: Keys, Secrets, OAuth
Gateways authenticate your API calls one of three ways:
- API keys (secret + publishable). Most common. Secret key goes on the server; publishable key on the client for tokenizing form inputs. Never put a secret key in the browser. Rotate both on a schedule; definitely on any suspected exposure.
- OAuth (client credentials). Used for platform-style gateways where your app acts on behalf of a merchant. Short-lived access tokens plus refresh tokens. More moving parts; stronger boundary.
- HMAC-signed requests. Every request is signed with a secret. Best for server-to-server integrations with high security requirements; more boilerplate.
Regardless of scheme: store secrets in a secrets manager (AWS Secrets Manager, HashiCorp Vault, 1Password Secrets Automation), never in code, never in env files committed to git. Scope keys narrowly (read-only, invoice-only, refund-only). Rotate quarterly.
Idempotency: The Pattern That Prevents Double Charges
An API call may fail in three ways: server never received it, server processed it and the response was lost, or server processed it and returned an error. You cannot tell which from the client. If you retry, you risk a second charge.
Idempotency solves this. The client generates a unique key per logical request (a UUID tied to the order) and sends it in the Idempotency-Key header. The server records the key with the response; a retry with the same key returns the original response without re-executing the work.
POST /v1/charges
Idempotency-Key: 8b7cfa4d-1e6c-4a12-9a3e-...
Content-Type: application/json
{"amount": 9900, "currency": "usd", "source": "tok_..."}
Implementation rules:
- Generate the key on the client side, once per logical request. Not per retry.
- Persist the key with the order record before making the call.
- Use UUIDv4 or a strong random string. Not timestamps. Not auto-increment.
- Idempotency is not eternal: gateways typically cache keys for 24 to 72 hours. After that a new attempt is a new request.
- Test retries explicitly. Send the same key twice; assert you get one charge.
Webhooks: Signed, Asynchronous, Idempotent
Webhooks are the asynchronous counterpart to the API: the gateway calls your endpoint when an event happens (payment completed, dispute opened, payout settled). Done wrong, they cause silent failures; done right, they are the backbone of reliable payment state.
The non-negotiables:
- Verify the signature. HMAC-SHA256 of the raw request body using a shared secret, compared against a
X-Signatureheader. Reject any request whose signature does not validate. Never trust source IP alone. - Return 200 fast. Acknowledge receipt in under 500ms. Push the heavy lifting - DB writes, email sends, downstream API calls - to a background queue. Slow handlers time out and get retried, amplifying load.
- Be idempotent on event ID. Every webhook has a unique event ID. Record it before processing. If you see a duplicate, return 200 without re-processing.
- Handle out-of-order events. The webhook for "payment succeeded" can arrive before the API response that created the charge. Design state machines that tolerate any order.
- Expose a replay tool. Operators need to re-fire a specific event from the dashboard when your endpoint was down.
// Express.js pseudocode
app.post('/webhooks/gateway', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['x-signature'];
const expected = hmac('sha256', SECRET, req.body).toString('hex');
if (!timingSafeEqual(sig, expected)) return res.status(401).end();
const event = JSON.parse(req.body);
if (await seenEvent(event.id)) return res.status(200).end();
await enqueue(event);
return res.status(200).end();
});
Error Handling and Retries
Categorize errors by whether they are worth retrying:
| Category | Example | Retry? |
|---|---|---|
| 4xx client | Invalid card, missing parameter | No - fix the request |
| 402 payment required | Issuer decline | No - surface to user |
| 409 conflict | Idempotency key collision | No - treat as duplicate, fetch existing |
| 429 rate limit | Too many requests | Yes - exponential backoff, respect Retry-After |
| 5xx server | Gateway timeout, internal error | Yes - backoff, always send the same idempotency key |
| Network timeout | Connection reset, DNS failure | Yes - retry with idempotency key |
For retryable errors, use exponential backoff with jitter. A reasonable schedule: 1s, 3s, 10s, 30s, 2min, 10min, then stop and alert. Never retry synchronously in the request handler; always queue.
Sandbox, Test Cards, and Testing Strategy
Every major gateway provides a sandbox environment with test cards that trigger specific scenarios:
- 4242 4242 4242 4242 - generic success.
- 4000 0000 0000 0002 - generic decline.
- 4000 0000 0000 9995 - insufficient funds.
- 4000 0027 6000 3184 - 3DS2 required.
- 4000 0000 0000 0341 - attach, then decline on charge.
Testing discipline:
- Every production code path gets a sandbox test. No exceptions for "simple" code.
- Simulate every webhook event, including out-of-order and duplicate deliveries.
- Force errors by design: a test helper that replays a payment with a chosen decline code.
- Test idempotency: send the same request twice, assert exactly one charge.
- Test refunds, partial refunds, and disputes.
- Validate with a chaos test: kill the worker while a webhook is being processed, confirm it re-processes cleanly on restart.
Crypto Gateway API Specifics
The patterns above apply identically to crypto gateways. The differences are in the event model, not the plumbing:
- Invoice lifecycle, not charge lifecycle. Create an invoice with fiat amount; the buyer picks an asset; the gateway returns a deposit address or payment URI. Events:
invoice.created,payment.detected,payment.completed,payment.settled,invoice.expired,invoice.underpaid. - Confirmations as distinct events. Good gateways emit an event on mempool detection and another on final confirmation. Treat them separately in your state machine.
- No card tokens. No vaulting, no 3DS, no network tokenization. Replaced by wallet signatures, which are handled by the buyer's wallet, not your code.
- Refunds as outbound transactions. A refund requires you to initiate an on-chain transaction from your wallet or the gateway's wallet to the buyer's address. Plan for the buyer's address to have changed since the original payment.
- Off-ramp events.
payout.initiated,payout.paid,payout.failed. Reconcile these against your bank statements; the match is one payout per many invoices.
The happy-path code for a crypto integration is shorter than a card integration because there is no 3DS, no vaulting, and no chargeback flow. The edge cases are different but not more numerous.
Pre-Launch Integration Checklist
- Secrets stored in a secrets manager, never in code.
- Idempotency keys generated and persisted for every POST that causes money movement.
- Webhook signature verification in place and unit-tested against tampered payloads.
- Webhook handlers return 200 in under 500ms, process async.
- Event IDs deduplicated before processing.
- State machines tolerant of out-of-order events.
- Exponential backoff with jitter on all retryable errors.
- Dashboards for failed webhooks, failed payments, and failed payouts.
- Alerts on sustained error rates, not on single failures.
- Runbooks for the top 5 incident types you can imagine.
- Load-tested at 5x expected peak.
- Disaster test: kill workers mid-transaction; verify clean recovery.
Boost Your Business by Accepting Crypto Payments
Get Started
Frequently Asked Questions
Idempotency means a repeated request with the same idempotency key produces the same result without double-charging. The client sends a unique Idempotency-Key header per logical request; the server recognizes duplicates and returns the original response.
Compute an HMAC-SHA256 over the raw request body using the shared webhook secret, and compare it (using constant-time comparison) against the signature header. Reject any request whose signature does not validate, regardless of source IP.
HTTP 200 as fast as possible, ideally under 500ms. Push the actual work to a background queue. Webhook handlers that do business logic inline time out under load and cause cascading retries.
With automatic retries using exponential backoff - typical schedules are 1s, 5s, 30s, 2min, 15min, 1h, 6h, 24h. After exhausting retries the webhook is marked failed and available for manual replay in the dashboard.
Rely on webhooks as the source of truth. Use polling as a backstop: a scheduled job that reconciles any in-flight transaction whose final state is missing. Never rely on polling as the primary signal.
The publishable key is safe in the browser and is used to tokenize card data on the client. The secret key lives on your server and is used for everything else. Never expose the secret key publicly; rotate both on any suspected compromise.
Use the gateway's designated 3DS test cards (varies by vendor; usually a well-documented number like 4000 0027 6000 3184). The sandbox simulates the challenge flow and returns frictionless success or challenge-required per the card.
Same REST + webhook patterns. Different event names (invoice lifecycle instead of charge lifecycle), no 3DS or vaulting, and refunds are outbound on-chain transactions. The integration discipline - idempotency, signed webhooks, async processing - is identical.
Call POST /refunds with the original charge ID. For crypto, include the buyer's return address (which may have changed from the one that paid). Refunds are asynchronous; watch for refund.completed or refund.failed webhooks before notifying the customer.
Doing business logic inside the webhook handler instead of enqueuing. It works in dev, it fails under load in prod. Return 200 fast, process async; your ops team will thank you.