Est.

Simulating Stateful APIs Locally Without a Production Dependency

Test cumulative API workflows against a local simulator that persists state.

· 8 min read
Cover illustration for “Simulating Stateful APIs Locally Without a Production Dependency”
Integration Testing · October 29, 2026 · 8 min read · 1,822 words

To integration-test stateful APIs without hitting production, run the application against a local simulator that persists resources, enforces state transitions, and emits webhooks across sequential calls. Tests must assert the workflow’s cumulative outcome. Removing the production dependency is reliable only when the simulator’s behavior is regularly compared with the real API.

Why stateless mocks fail multi-step API workflows

A customer creation test sends POST /customers and receives 201 with {"id": "cust_123"}. The next request, GET /customers/cust_123, returns 404 because the create handler never stored the customer. The response looked valid in isolation. Yet the application needs the returned ID to point to a resource as well as fit a response schema. Separate handlers cannot establish that relationship unless they share state.

A payment test sends POST /payments, receives 201 with a pending status, and reads it back through GET /payments/{id}. Then POST /payments/{id}/capture returns 200 with captured, but the next read still returns pending. Nothing connects capture to the read handler. If the test checks only the capture response, CI can pass immediately before a deployment that fails against the real API’s constraints.

What stateful simulation actually means and why the distinction matters

A stateful simulator maintains a shared model of resources across requests. Creation changes what subsequent reads can retrieve. Updates change that stored model, and state-machine rules say whether a requested transition is valid. Relational consistency means a subscription created for a customer still references that customer when read later. Webhook emission carries those changes beyond the request-response exchange by calling the application’s event handler with the event that matches the change.

This changes what an integration test can prove. Rather than arranging independent answers, the test exercises operations whose effects accumulate in the simulated API. A charge’s transition from pending to succeeded must follow the model’s rules instead of appearing because a handler was configured to return that word. A mock can establish that the application works with its configured responses, but that alone does not establish that the application respects the external API’s workflow.

How state propagates through a realistic multi-step flow

A Stripe-style payment flow starts with POST /customers. The simulator stores the customer and returns an ID that later requests can retrieve. At this point, listing customers in a clean instance should reveal exactly one record. That intermediate observation has a specific purpose. It establishes that the simulated customer exists before the application uses its ID to create a dependent resource.

Next, POST /payment_intents includes the returned customer ID. The simulator validates that the customer exists and creates the intent in requires_payment_method state. Substituting a fixture’s unrelated customer ID would avoid testing whether the application passes the actual result downstream. Stripe’s lifecycle documentation distinguishes this initial state from requires_confirmation, which is entered after the customer provides payment information.

Once the payment method requirement is satisfied, POST /payment_intents/{id}/confirm advances the worked flow to succeeded and stores that change. The simulator then emits payment_intent.succeeded, exercising the application’s webhook handler with a correctly structured payload. A final GET /payment_intents/{id} must retrieve the updated intent in the succeeded state. Together, the returned IDs, stored relationships, transition, and event make this a workflow test rather than a sequence of unrelated HTTP assertions.

Where stateless tools still belong in the stack

Stateless mocks remain appropriate for narrower testing claims. They can check whether a function uses the correct URL, method, and headers, or whether it handles an isolated 401, 429, or 503 response. They also unblock frontend development before an endpoint exists and stand in for dependencies without a public specification or sandbox. These are useful boundaries to test. None requires a simulated resource to survive between calls.

Prism generates schema-conformant responses from an OpenAPI specification and returns 404 for routes the specification does not describe. That makes missing routes easy to spot, though the mock still relies on the specification being up to date. Teams can use lightweight stubs in the early CI stages and stateful workflow tests at the integration stage. The tools answer different questions. Fast HTTP-client checks need not bear the cost or responsibility of validating cumulative integration behavior.

What a simulator needs to reflect actual API behavior, not just its shape

Persistence does not guarantee fidelity to the real API. A simulator may remember every created resource while allowing an invalid transition, omitting a response field, or returning the wrong error envelope. Such discrepancies let integration defects pass CI. The danger is greater when teams treat statefulness as evidence of accuracy, because stored data makes the simulation appear more complete than it is.

The simulator supplies the dependency during development and CI, while contract tests compare its assumptions with the real API. If a provider changes a field format or adds a required header, local tests cannot discover that change by exercising an unchanged simulator. Even when routine CI makes no live calls, the tests still need to run against a real sandbox from time to time.

One way to detect drift is to run the full suite against that sandbox from time to time, record HTTP requests and responses, and commit those recordings as fixtures. Those contracts provide evidence against which simulated behavior can be assessed as the provider changes. Representative data is another part of fidelity. An empty instance cannot meaningfully exercise pagination, filtering, sorting, or relationship traversal, even if every response has the right schema. Pre-seeded realistic data exposes paths that a successful create-and-read test leaves untouched.

Fault injection as a first-class concern, not an afterthought

Successful responses cover only part of an integration’s contract. Under rate limiting, an upstream 429 with a Retry-After header should cause backoff rather than an immediate retry that compounds the problem. Authentication expiry requires a different response from the application. An OAuth token error represented by 400 Bad Request and a realistic invalid_grant payload should cause the application to report needs_reauth and avoid silent retries. These failures require distinct assertions.

Idempotency makes failure testing inseparable from persistent state. If payment creation times out, the application should retry with the same idempotency key without creating a duplicate payment. A simulator must store and enforce that key to test the outcome. Returning a canned timeout is not enough. Fixed delays, random delays, and connection drops also test timeout handling and circuit breakers without causing failures in real infrastructure.

Running with a flag such as --scenario auth_failure exposes how failure handling differs between endpoints, rather than testing only the response a developer remembered to configure. An application might handle 401 correctly for customers but not for payment intents. A workflow-wide scenario can expose that inconsistency. Isolated error mocks remain useful, but they do not by themselves establish how failures interact with accumulated state.

How do you run stateful API integration tests in CI without a live dependency?

The test run’s exit code controls whether deployment can go ahead. Code 0 means all workflows passed, and code 1 means a test failed. Routine execution needs no live API credentials, incurs no upstream rate limits, and does not wait for a third-party service to recover.

Isolation makes that result repeatable across runs. Starting a container through Testcontainers or a Docker Compose step gives each CI run its own clean instance, so jobs that run at the same time do not share resources. The testing setup can also keep runs separate by resetting state or using per-session isolation. Previous runs must leave no inherited residue. Otherwise, a test may retrieve a customer that it failed to create itself, masking precisely the dependency the suite is intended to exercise.

Running locally also cuts out external network round-trips and the authentication overhead that comes with them. That advantage compounds in suites with many sequential calls, including agent workflows. For offline or air-gapped environments, the runtime boundary is stricter. The simulator must run without an external connection, including one to a third-party sandbox. Periodic verification must be separate maintenance work so that every build can run without it.

Stateful simulation for AI agents that call external APIs

An agent creating a GitHub issue, linking it to a project, and adding a label has the same ID-chaining requirement as a conventional integration. A stateless response can return an issue ID without creating an issue that later operations can use. The LLM adds another source of variability, but it does not change the external API’s requirement for consistent state.

For agent tests, exact output-string matching makes that variability a source of flakiness. The recommended approach records the ordered tool calls and their arguments as a trajectory and compares it with a committed golden file. Stateful simulation stabilizes the environment in which those calls execute. It does not make the LLM deterministic.

The testing architecture can retain a real LLM and a real MCP server while directing the server’s third-party API calls to the simulator. This exercises pagination, rate limits, error envelopes, and authentication refresh logic without contacting the external API. An inspectable call log also exposes unsupported claims. If the agent says it created a record but made no POST call to the relevant endpoint, the claimed action has no corresponding operation in the test environment.

Reinforcement learning increases the scale of this requirement. Training tool-using agents requires millions of environment interactions, while live APIs impose latency and rate limits. Web search calls, for example, can take seconds, and the number of queries allowed per minute is capped. Those constraints make live endpoints impractical for training at scale. A deterministic local simulator that scales horizontally and has no per-interaction API charge provides an environment suited to those repeated workflows.

What to look for when evaluating or building a stateful simulator

Evaluation should begin with executable acceptance tests. The four-step payment capture diagnostic tests whether a mutation actually changes the next read. A transition test should also reject an attempt to move a completed payment back to pending when that violates the modeled API. Acceptance requires both outcomes and constraints. Webhook checks should establish that resource changes exercise the application’s event handler, rather than bypassing it with a separately supplied payload.

To make that behavior usable in CI, the candidate must start quickly, reset between runs, report failure reliably, and function without external network access. Seed data that reflects real use and named failure scenarios show whether the suite can test more than the successful path. Parallelism needs its own scrutiny. Large suites and reinforcement-learning environments require many containers to run without shared state, or simultaneous workflows will interfere with one another.

Building from an OpenAPI specification can provide a starting point, but the initial implementation is not the largest ongoing obligation. Keeping behavior aligned with an evolving provider is the majority of the work. Adopting a verified simulator transfers that maintenance responsibility to the vendor. The final evaluation questions are therefore how often verification runs, what real API behavior it compares, and what evidence the latest run provides. A simulator is only as trustworthy as its last verification run.

More in Integration Testing