API Integrations

Connecting Systems That Weren't Designed to Talk

Modern businesses run on connected systems. Your application talks to payment processors, email services, accounting software, CRMs, and a dozen other external services. Each connection is an API integration, and each one is a potential point of failure.

Good API integration is invisible. Data flows between systems automatically. Orders appear in your accounting package. Payment confirmations trigger fulfilment workflows. Customer records stay consistent across every platform. Bad API integration creates operational headaches: failed payments that never retry, missing customer data, orders that vanish between systems.

We build and maintain API integrations across a wide range of Laravel applications, using the Laravel HTTP client and Guzzle under the hood. Payment processors, shipping providers, accounting platforms, marketing tools, government services. The difference between an integration that works in a demo and one that works in production comes down to engineering decisions made before the first line of code is written.

If your systems need to talk to each other reliably and you want to understand what "reliably" actually means in practice, we are happy to talk it through.

Production rules. A timeout does not mean failure. Persist inbound events before processing them. Acknowledge webhooks fast; do the work asynchronously. Idempotency is keyed to the business operation, not the HTTP request. Calibrate circuit breaker thresholds to your request volume, not a tutorial's defaults.


Why API integration is harder than it looks

Every API integration has the same underlying problem: you are coupling your system to something you do not control. The external service can change behaviour, go offline, rate-limit you, return unexpected data, or deprecate the endpoint you depend on.

This is not a solvable problem in the sense that you can eliminate it. It is a constraint you design around. The goal is not to prevent external services from failing. The goal is to ensure that when they fail, your system continues to operate in a predictable, recoverable state.

Four things make API integration consistently difficult:

You do not control the other side

External APIs change. Providers update their systems, modify response formats, deprecate endpoints. Sometimes they notify you. Sometimes they do not. Your integration needs to handle versions you did not anticipate and degrade gracefully when fields disappear.

Networks are unreliable

Requests fail. Timeouts happen. Services go down. Packets get lost. DNS resolves incorrectly. TLS handshakes fail. An integration that works perfectly in development can fail unpredictably in production. You must assume failure and design for it.

Data does not map cleanly

Your system's concept of a "customer" might not match theirs. Field names differ. Required fields in one system are optional in another. Date formats vary. Currency precision differs. Keeping a single source of truth across connected systems requires deliberate data modelling and explicit mapping at every boundary.

Errors are ambiguous

Did the request succeed? Fail? Partially succeed? A timeout does not mean failure. A 500 error might have processed the request before erroring. Different APIs communicate errors differently. Error handling must interpret responses correctly regardless of how they are formatted.


What goes wrong with naive API integration

Most integration code starts the same way. A developer reads the API documentation, writes a function that makes an HTTP request, parses the response, and moves on. The code works in development. It works in the first few weeks of production. Then it fails.

The naive approach treats the external API like a local function call: synchronous, reliable, and consistent. This assumption is false in every particular.

No connection timeout configured. HTTP client uses default timeout (often 30 seconds or infinite). A hung connection blocks threads, exhausts connection pools, and cascades into system-wide failure.
No retry logic. Transient failures become permanent failures. Users retry manually, creating duplicate operations.
No idempotency. Retries create duplicate records, duplicate charges, duplicate emails. The customer gets charged twice. The order gets placed twice.
Inline execution. API calls happen in the request-response cycle. Slow external APIs make your application slow. Failing external APIs make your application fail.
No logging. When something fails, no one knows what was sent, what was received, or how long it took. Debugging becomes guesswork.
Tight coupling. API client code is scattered throughout the codebase. When the external API changes, dozens of files need updating.

These are not edge cases. They are the normal operating conditions of any third-party API integration that runs long enough in production.


What good API integration looks like in production

Reliable API integration requires explicit design decisions about failure modes. Every external call needs answers to specific questions before writing code. What happens if this request times out? What if the service is down for an hour? What if the response is malformed? What if we get rate-limited? What if the same request is submitted twice?

We assume failure

Every API call can fail. We design for it from the start. Integrations that assume success work in demos. Integrations that assume failure work in production. Before implementing any integration, we define the failure envelope: every way the call can go wrong and what the system does in each case.

We make operations idempotent

If a request fails and we retry, the same operation must not happen twice. We use unique identifiers (idempotency keys) to prevent duplicate charges, duplicate orders, and duplicate records. Most payment processors and financial APIs support this natively, as documented in Stripe's idempotent requests guide. For APIs that do not, we implement idempotency on our side by tracking operations in a database before sending them.

We log everything (safely)

When an API integration fails at 3am, the logs need to tell us exactly what happened. We log request details, response details, timing, outcome, and a correlation ID linking the request to the business operation that triggered it. Sanitisation is critical: logs must not contain API keys, passwords, credit card numbers, or personal data. For more on maintaining complete operational records, see our approach to audit trails.

We isolate external dependencies

We wrap external API calls in abstraction layers (the adapter pattern applied to external services). The rest of the codebase interacts with our wrapper, not the external API directly. When the external API changes, we change the wrapper. Every other file remains unchanged. This pattern also lets us mock the external API for testing, swap providers without touching business logic, and add logging and retry logic in one place.

A note of realism: provider swapping works for fungible services (email delivery, SMS). For payments and accounting, it is largely fiction. Stripe and GoCardless have different customer models, different webhook event structures, different idempotency semantics. An abstraction layer still helps (isolation, testability, centralised error handling), but "swap providers with a config change" is not a realistic promise for non-fungible integrations.


REST API integration patterns for different scenarios

Different integration scenarios call for different patterns. The choice depends on latency requirements, reliability needs, and the capabilities of the external system. We use four primary patterns across our custom web applications.

Synchronous request-response

Send a request, wait for a response, proceed based on the result. Used for real-time data lookups and synchronous operations where the user is waiting (payment authorisation, address validation). The risk is that your application blocks waiting. We mitigate with aggressive timeouts, circuit breakers, and fallback behaviour.

Asynchronous with webhooks

Send a request, receive acknowledgement that it was queued, then receive results via webhook callback. Webhook integration requires particular care: endpoints must verify that requests come from the expected source using HMAC-SHA256 signature verification, acknowledge quickly (return 200 within one second), store the raw payload, and process asynchronously. Duplicate webhooks are normal, so processing must be idempotent. Webhook ordering is also not guaranteed: Stripe's payment_intent.succeeded can arrive before checkout.session.completed, which means your handler needs a state machine that tolerates out-of-order events rather than assuming sequential delivery.

Queue-based processing

Decouple the triggering of an operation from its execution. The request goes into a queue, a background job processes it, and the result is stored or forwarded. Laravel's queue system handles retry logic and delayed dispatch out of the box; rate limiting and dead letter routing require additional configuration (job middleware for throttling, and failed job handling or SQS DLQ setup for dead letters). Laravel Horizon provides real-time monitoring of queue depth, job duration, and failure rates. When an external API has a bad hour, queued operations wait and retry rather than failing immediately.

Polling

Periodically check an external system for updates. Used when the external system does not support webhooks, when webhook delivery is unreliable, or when you need to synchronise data that changes outside your control. We implement polling with exponential backoff during quiet periods, proper pagination for large datasets, and state tracking to process only changed records.

Requirement Pattern Trade-off
User waiting for result Synchronous Must handle timeouts gracefully
Operation takes more than 5 seconds Async with webhooks or polling More complex state management
High volume (1000+ calls/min) Queue-based Latency between trigger and execution
External system unreliable Queue with dead letter Need monitoring and manual review
Need eventual consistency Polling or event-driven Data may be stale between sync cycles

For a deeper look at how these patterns compose in practice, see our guide to integration patterns.


Error handling, rate limiting, and security

The operational concerns of API integration are where most implementations fall short. Error handling, rate limiting, and security are not afterthoughts. They are load-bearing parts of the design.

Error classification and retry logic

Different errors require different responses. We classify errors into four categories:

Transient errors (retry immediately)
Network timeouts, connection refused, DNS failures, 502/503/504 responses. These often resolve on retry with exponential backoff.
Rate limit errors (retry with delay)
429 Too Many Requests. The request is valid but we are sending too many. Respect the Retry-After header; aggressive retry makes the problem worse.
Client errors (do not retry)
400, 401, 403, 404, 422 responses. The request is malformed or unauthorised. Retrying will not help. Log and alert for investigation.
Ambiguous errors (retry with idempotency)
Timeouts where we do not know if the request was processed, 500 errors that might have partially succeeded. Retry with idempotency keys and verify state before proceeding.

Our retry implementation uses exponential backoff with jitter and a backoff ceiling: immediate first attempt, then delays of 1s, 2s, 4s, 8s before moving the operation to a dead letter queue for manual review.

Error class Retry? Delay strategy Idempotency required?
Transient (502/503/504) Yes Exponential backoff with jitter Yes, for state-changing operations
Rate limit (429) Yes Respect Retry-After header Yes
Client (400/401/403/422) No N/A N/A
Ambiguous (timeout, 500) Yes, with verification Check state first, then retry Always

Circuit breakers and rate limiting

If an external service is failing repeatedly, continuing to call it wastes resources and can cascade into broader failure. The circuit breaker pattern detects repeated failures and stops calling the failing service for a period, giving it time to recover. Three states: closed (normal), open (fail fast), half-open (testing recovery). Circuit breakers prevent one bad third-party API integration from bringing down your entire application. This matters especially when you are scaling without chaos: a single failing dependency should not drag everything else down with it.

Most tutorials suggest textbook thresholds (5 failures in 60 seconds). This is dangerous for low-volume integrations. At 2 requests per minute, 5 failures would take over two minutes to accumulate, meaning the time window is irrelevant and a consecutive failure count is more appropriate. At 1,000 requests per minute, 5 failures in 60 seconds means 0.5% failure rate, which is noise. The point: use failure rate (percentage of requests failing) for high-volume integrations and consecutive failure count for low-volume ones. Do not copy thresholds from a blog post.

Most external APIs have rate limits. We implement client-side throttling (token bucket or sliding window) to stay within limits rather than hitting limits and dealing with errors. This is more efficient and more reliable.

Authentication and credential management

API credentials are keys to external systems. They never belong in code or version control. We load credentials from environment variables or secret management systems at runtime, implement OAuth 2.0 with proper token refresh scheduling (including proactive refresh before expiry and graceful handling of revoked permissions), and scope permissions to the minimum required. All API calls go over TLS 1.2 or higher with certificate validation enabled. For multi-tenant applications, credential isolation between tenants is non-negotiable: one tenant's expired token must never affect another. For the broader operational picture on keeping credentials and infrastructure secure, see our security and ops guide.


Testing integrations without relying on live services

Mocking external APIs for unit tests is the easy part. The hard part is building confidence that your integration handles real production conditions: schema drift, partial failures, and the exact sequence of events that cause data corruption.

We use a layered testing strategy. Each layer catches a different class of problem.

Sandbox environments

Stripe, GoCardless, and Xero all provide sandbox APIs for happy-path verification. Sandboxes confirm your code can make a successful request. They do not confirm your code handles the failure modes that matter most, because sandboxes rarely simulate timeouts, rate limits, or malformed responses accurately.

Recorded response mocking

Capture real API responses and replay them in CI. This gives deterministic tests that run without network access, catch regressions quickly, and cover error responses that sandboxes will not generate. The risk is staleness: if the provider changes their response format, your recorded mocks will not tell you.

Contract testing

Schema drift detection: periodically validate that the external API's actual response structure (whether described by an OpenAPI Specification or inferred from recorded responses) still matches your expectations. When a provider adds a required field, removes an optional one, or changes a data type, contract tests catch it before production does. This is the gap between "it worked in CI" and "it works against the live API."

Failure injection

Deliberately inject timeouts, connection resets, and malformed responses to verify that circuit breakers trip, retries fire, payload validation catches corrupted data, and dead letter queues catch what cannot be recovered. Without this, your resilience code is untested code, and untested code does not work when it matters.

None of these layers alone is sufficient. Sandboxes verify happy paths. Mocks verify known failure paths. Contract tests detect unknown changes. Failure injection verifies resilience. A well-tested integration uses all four.


Long-term maintenance: the cost nobody plans for

Most integration content focuses on the initial build. The real cost is maintenance. Integrations degrade over time as providers deprecate endpoints, change response schemas, tighten rate limits, or alter OAuth scope requirements. An integration that nobody actively owns will eventually break, and you will discover it from a customer complaint, not a monitoring alert.

We treat integration maintenance as an ongoing engineering concern, not a one-off project.

Monitor deprecation headers. Many providers (Stripe, Xero) send deprecation warnings in HTTP response headers before removing endpoints. We log and alert on these so the team has advance notice, not a surprise outage.
Pin API versions. Where the provider supports versioning (Stripe's API version header, Xero's v2 endpoints), we pin to a specific version and upgrade deliberately, testing the new version against our contract tests before switching.
Schedule credential rotation. OAuth tokens expire. API keys should rotate on a regular schedule. We track expiry dates and automate rotation where possible, catching expired credentials before they cause production failures.
Run quarterly integration reviews. Each integration gets a periodic health check: is the API version still supported? Have rate limits changed? Are there new OAuth scope requirements? Has the provider published any migration guides? This is the discipline that prevents "it worked for two years and then stopped" incidents.

For applications with audit trail requirements (payments, compliance, regulated data), integration maintenance is not optional. A broken sync between your application and your accounting platform does not just cause inconvenience. It causes reconciliation failures that compound over time.


Reconciliation: when "did it work?" has no clear answer

Ambiguous failures are the hardest class of integration problem. A request times out. The payment may or may not have been processed. A webhook arrives late, or out of order, or not at all. Your database says "pending" but the provider says "succeeded." These are not edge cases. They are normal operating conditions for any integration that processes financial transactions or state-changing operations.

Reconciliation is the discipline of detecting and resolving these mismatches before they compound.

Pull-to-confirm

After a timeout or ambiguous error, query the provider to determine the actual state. If you sent a payment request and timed out, call the provider's API to check whether the charge was created. Do not assume failure and retry without checking: that is how customers get charged twice.

Settlement vs request state

Your database tracks request state (sent, pending, confirmed). The provider tracks settlement state (authorised, captured, settled, refunded). These can diverge. A scheduled reconciliation job compares your records against the provider's records and flags mismatches for review.

Operator review queues

Some mismatches cannot be resolved automatically. A dead letter queue collects operations that exhausted retries or produced contradictory state. An operator reviews them with full context: what was sent, what the provider returned, what the current state is on both sides. This is where structured audit logs pay for themselves.

Reconciliation connects directly to your single source of truth strategy. If two systems disagree about whether a payment was processed, which one is authoritative? That question needs an answer before you write the integration, not after the first discrepancy surfaces.


Third-party API integration for small business

If you run a small or medium-sized business, you probably do not need an enterprise integration platform. You need specific systems connected to each other reliably: your payment processor to your accounting software, your CRM to your email marketing, your order management to your shipping provider.

The challenge for smaller businesses is that integration problems hit harder. A large enterprise has a dedicated team to monitor and fix integration failures. When your Stripe-to-Xero sync breaks at a 20-person company, it might be days before someone notices the invoices stopped reconciling.

This is where API integration for small business differs from enterprise integration. You need:

  • Integrations that recover automatically Retry logic and queue-based processing mean transient failures resolve without human intervention.
  • Monitoring that tells you before customers do Alerting on error rates, latency, and queue depth catches problems early.
  • Integrations that are maintainable Clean abstraction layers mean your team (or ours) can update integrations when external APIs change, without rewriting the application.
  • Integrations that are documented When something does need attention, clear documentation and structured logs mean anyone on the team can diagnose the issue.

These patterns apply across all common integration targets: payment processors (Stripe, GoCardless, PayPal), accounting software (Xero, FreeAgent), CRMs (HubSpot, Salesforce), email services (Postmark, Mailgun), shipping providers, government APIs, and industry-specific systems. The integration patterns are consistent even when the specific APIs are not. Where integrations trigger multi-step business processes (order received, payment confirmed, fulfilment started, customer notified), they connect to broader business process automation and workflow engine patterns.


Connect your systems

We build API integration services that connect your systems for the real world, where networks fail, APIs change, and services go down. Not brittle connections that break at the first timeout. Integrations designed for production, with proper failure handling and full visibility. Our integration service covers the full scope: connecting your existing tools, building custom middleware, and maintaining the connections long-term.

Talk through your integration requirements →
Graphic Swish