SaaS & Architecture

Integrating Third-Party APIs: Stripe, Twilio, and Beyond

A
Abdul Rahaman
3 September 2026
11 min read
API integrationStripeTwiliowebhooksbackend developmentSaaS & Architecture

Integrating Third-Party APIs: Stripe, Twilio, and Beyond

The Stripe documentation is excellent. The Twilio quickstart gets you sending an SMS in twenty minutes. Razorpay's checkout API is well-structured and the SDKs are straightforward. Third-party API integrations look, at the beginning, like tasks that take a few days and work reliably from there.

Then you reach production. A payment webhook arrives twice and charges a user twice. A Twilio delivery callback never fires and your system thinks the message failed when it was delivered. An OAuth token expires at 3 AM and your integration silently stops working until someone notices the next morning. A provider silently renames a field in their response and your application crashes because it was reading a key that no longer exists.

These are not edge cases. They are the normal failure modes of production API integrations, and almost every team hits some version of them. The difference between an integration that works reliably and one that creates ongoing operational incidents is a set of architectural decisions that the documentation never emphasises as much as it should.


Why Third-Party Integrations Are Harder Than They Look

The core challenge is that you are building a dependency on a system you do not control. When you call an external API, you're trusting that the network between you and them is reliable, that their servers are responding correctly, that their documented behaviour matches their actual behaviour, and that any change they make to their API will be announced in advance and won't break your code silently.

None of these things are guaranteed. Networks drop packets. Servers have degraded states that return 200 responses with unexpected body shapes. Providers deprecate fields by removing them from responses without always updating the documentation first. OAuth tokens expire on their own schedule and need refresh logic that has to handle every race condition.

The pattern that resolves most of these problems is the same in almost every case: don't trust the external system, design defensively, and build recovery mechanisms rather than assuming success.


Webhooks: The Pattern Most Teams Get Wrong

Webhooks are the mechanism by which providers notify your system when something happens — a payment completes, a message is delivered, a subscription renews. Understanding webhooks correctly is arguably the most important aspect of any third-party integration.

The common mistake: writing a webhook handler that receives the event and immediately performs the work synchronously — queries the database, creates records, sends emails, calls other APIs — before returning a response.

This creates two problems. First, it's slow, and providers like Stripe expect your webhook endpoint to respond within a few seconds. A slow response looks like a timeout to the provider, who will then retry the event — now you've processed the same event twice. Second, if any step in that synchronous chain fails, the webhook handler returns a non-200 status, the provider retries, and you're processing the event again under the same conditions that caused the failure.

The correct pattern — receive fast, process safely — is straightforward:

  1. Receive the webhook — verify the signature (see below), write the raw event to a queue or a database table, return 200 OK immediately. This should take under 100ms.
  2. Process the event asynchronously — a background worker reads from the queue, processes the event, and handles any failures independently from the webhook receipt.
  3. Make processing idempotent — before processing any event, check whether it has already been processed. Providers may deliver the same event multiple times. If your event processing is idempotent, duplicates are harmless.

The signature verification step deserves explicit attention. Stripe signs every webhook with an HMAC-SHA256 hash of the payload using your webhook signing secret. If you don't verify this signature, anyone who knows your webhook URL can POST fake events to your application. Always use the provider's SDK for verification — it handles the timing-safe comparison that prevents timing attacks.

// Node.js Stripe webhook verification — correct
const event = stripe.webhooks.constructEvent(
  request.rawBody,       // must be the raw, unparsed body
  request.headers['stripe-signature'],
  process.env.STRIPE_WEBHOOK_SECRET
);

The common mistake here is passing a parsed JSON body instead of the raw body — the signature verification will fail for every event if the body has been parsed and re-serialised.


Idempotency: The Concept That Prevents Duplicate Charges

Idempotency is the property of an operation where performing it multiple times produces the same result as performing it once. For payment APIs, this is not optional — it's what prevents charging a customer twice when a network timeout causes your server to retry a payment request that already succeeded.

The scenario: your server sends a charge request to Stripe. The network drops the response before your server receives it. Your server doesn't know if the charge was created or not. If you retry without an idempotency key, you may create a second charge. If you retry with the same idempotency key, Stripe returns the result of the original request — the charge that already succeeded — without creating another.

Stripe's idempotency implementation is robust: pass an Idempotency-Key header with a unique value for each intended charge. If you use the same key within 24 hours, Stripe returns the cached result.

// Correct: include idempotency key
const charge = await stripe.charges.create(
  { amount: 50000, currency: 'inr', customer: customerId },
  { idempotencyKey: `charge-order-${orderId}` }
);

The key should be deterministically derived from your business operation — charge-order-{orderId} means you can always construct the right key from your own data, and if a retry is needed, you generate the same key automatically.

For APIs that don't natively support idempotency keys, implement it yourself: before executing a write operation, check whether a record of the completed operation already exists in your database, keyed to the unique identifier of the request. If it does, return the existing result.


Retry Logic and Circuit Breakers

External APIs return errors. The question is what your code does when they do.

The wrong answer: crash with an unhandled exception, or retry immediately in a tight loop.

A tight retry loop under load creates a "thundering herd" problem — if a provider is struggling, a thousand concurrent clients immediately retrying hammers their servers and makes the outage longer for everyone.

The right pattern is exponential backoff with jitter:

  • First retry: wait 1 second
  • Second retry: wait 2 seconds
  • Third retry: wait 4 seconds
  • Add a random component (jitter) of ±30% to each interval to spread retries across clients

Categorise errors before deciding whether to retry:

  • 4xx errors (400, 401, 422): These are client errors — your request was malformed or you're not authenticated. Retrying without fixing the request will not help. Log, alert, and stop.
  • 5xx errors (500, 502, 503): These are server errors on the provider's side. Retry with backoff.
  • Network timeouts: Retry with backoff, but with idempotency keys to prevent duplicate operations.

For providers you call frequently from user-facing flows, a circuit breaker adds another safety layer. A circuit breaker tracks the error rate of outgoing calls to a provider. If the error rate exceeds a threshold (say, 50% of calls failing over 30 seconds), it "trips" — subsequent calls fail immediately with a fallback response rather than attempting the network call. This prevents your threads from blocking on calls that are destined to fail, and gives the provider time to recover. After a cooldown period, the circuit breaker enters "half-open" state, allowing a limited number of test requests through to check whether the provider has recovered.


Managing API Keys and Secrets

API keys are credentials. Treating them as anything less creates security exposure that can be difficult or impossible to fully remediate.

The minimum requirements:

Never commit API keys to source code. A key committed to a git repository — even once, even if deleted in a subsequent commit — may have been indexed by GitHub or a CI system and should be considered compromised. Rotate it immediately.

Use environment variables for local development, a secrets manager for production. AWS Secrets Manager, HashiCorp Vault, or your hosting platform's secrets management stores keys encrypted at rest and provides access logs. Environment variables in production deployment platforms (Vercel, Railway, Render) are acceptable if the platform encrypts them at rest.

Follow the principle of least privilege. Create separate API keys for separate purposes. Stripe supports restricted keys — a key used only for reading payment method data should not have permission to create charges. If a restricted key is compromised, the blast radius is limited.

Rotate keys regularly and have a rotation procedure. The rotation procedure should be documented and tested before you need it in an emergency. When a key needs to be rotated urgently (due to a suspected leak), you should be able to deploy a new key to production in under fifteen minutes.

For OAuth-based integrations — connecting your application to a user's Google account, Slack workspace, or similar — build automated token refresh logic from the start. OAuth access tokens typically expire in one hour. Your integration must handle token refresh transparently, and it must handle the case where the refresh token has also expired (the user has revoked access) gracefully — surfacing a re-authorisation prompt rather than silently failing.


The Silent Schema Change Problem

Third-party APIs evolve. Fields get added, renamed, or removed. Data types change. Response shapes are restructured in ways that their versioning policy technically permits.

If your code accesses response fields like response.data.customer.address.city without validation, a rename or removal of the address field will produce a null pointer exception or undefined reference — a silent failure mode until a user reports something broken.

Best practices:

Validate response schemas explicitly. Use a schema validation library (Zod in TypeScript/Node.js, Pydantic in Python) to validate the shape of API responses before your code accesses any field. If the response doesn't match the expected schema, catch it, log the raw response, and raise an alert — rather than crashing or silently dropping the operation.

Log raw responses for critical integrations. For payment and financial integrations especially, log the raw API response alongside your application's processed version. When something goes wrong, having the original response is invaluable for debugging whether the issue was in the provider's response or in your processing logic.

Monitor provider status pages and changelogs. Stripe, Twilio, Razorpay, and most major providers publish API changelogs. Subscribe to them. Breaking changes almost always come with advance notice — you should know about a deprecation before it affects your production system, not after.


A Practical Integration Checklist

Before any third-party integration goes to production:

CategoryRequirement
WebhooksSignature verification implemented
WebhooksAsync processing with event queuing
WebhooksIdempotency check before processing
WebhooksReconciliation job for missed events
RequestsRetry logic with exponential backoff + jitter
RequestsCircuit breaker for high-frequency calls
RequestsIdempotency keys on all write operations
RequestsExplicit timeouts set (not library defaults)
SecurityAPI keys in secrets manager, not env vars in code
SecurityPrinciple of least privilege on API key scope
SecurityOAuth token refresh logic implemented and tested
ReliabilitySchema validation on response parsing
ReliabilityRaw response logging for financial integrations
ReliabilityAlerts on 4xx/5xx error rate spikes
ReliabilityProvider status page monitoring configured

FAQ

Why does Stripe sometimes deliver the same webhook event more than once? Stripe retries webhook delivery if your endpoint responds with a non-2xx status or doesn't respond within a few seconds. Network issues, slow response times, or errors in your handler can all trigger retries. This is by design — Stripe's guarantee is "at least once" delivery, not "exactly once." Your webhook handler must implement idempotency (checking whether the event has already been processed) to handle duplicates safely.

What is an idempotency key and when should I use one? An idempotency key is a unique identifier you include with a write request that tells the provider "if you see this key again, return the result of the original request rather than executing the operation again." Use idempotency keys on every payment request, order creation, or any write operation where executing twice would cause a real problem — duplicate charges, duplicate orders, duplicate notifications. Derive the key deterministically from your business data (e.g., payment-invoice-{invoiceId}) so you can reconstruct the correct key if a retry is needed.

How do I handle a third-party API that goes down during a user-facing operation? Design your system so the critical path for the user doesn't depend on the external API responding synchronously in the request. For non-real-time operations (sending a notification, processing a payment asynchronously), queue the operation and process it when the API is available. For synchronous flows (a checkout that requires a payment to confirm), surface a clear error to the user and allow them to retry, rather than hanging indefinitely waiting for the provider to respond.

What's the difference between a 401 and a 403 error from a third-party API, and how should I handle each? A 401 (Unauthorized) means your credentials are not valid — either the API key is wrong, expired, or not included. Check your key, verify it hasn't been rotated, and do not retry automatically. A 403 (Forbidden) means your credentials are valid but you don't have permission for the specific action — either the API key lacks the required scope or the operation isn't permitted for your account tier. Both require investigation and a fix before retrying.

How do I test webhook integrations locally during development? The Stripe CLI and the Twilio CLI both support webhook forwarding — they establish a connection to your local server and forward production or test events from the provider to your local webhook endpoint. This is the recommended approach. Using these tools, you can test your webhook handler against real event payloads, including edge cases like failed payments, disputed charges, and undelivered messages, without deploying to a staging environment.


Building integrations that work reliably in production, not just in the happy path, requires the same engineering discipline as any other backend work. If your product needs complex integrations with payment gateways, communication APIs, CRMs, or custom third-party systems, get in touch with our engineering team — we've built and maintained production integrations across a wide range of providers and can scope what yours requires.


Custom Software & API development · API-first development guide · How to ensure 99.9% SaaS uptime

Author: Abdul Rahaman
Last updated: August 2026

Ready to Build This for Your Business?

Talk to our product team — we'll scope your idea and turn it into web, mobile, or custom software, then help it grow.

Start a Project