Skip to main content

A stateful API mock server for integration testing

Project description

imnot

imnot — stateful API mock server

CI codecov

imnot is a stateful API mock server for integration testing. A partner is any external API your service depends on — a booking platform, a payment gateway, a data feed. You describe it in YAML, and imnot turns it into a local mock server your tests can hit.

Define an external API as a YAML file, run imnot start, and you get a fully functional mock server — no code changes required to add new APIs or endpoints.

Why imnot?

  • YAML in, mock server out. One file defines an external API's endpoints, patterns, and responses. No code, no JVM, no GUI.
  • Stateful flows, not just fixed responses. OAuth, async submit/poll/fetch, webhooks, and per-test session isolation — all modeled in YAML, not scripted.
  • Lives next to your code. The mock definition is version-controlled alongside the integration it tests and runs anywhere Docker runs.

Reach for other tools when you need conditional responses based on request body content, or prefer a GUI-first workflow.

AI-ready

Don't want to write the YAML yourself? Paste one of these into Claude, ChatGPT, or your AI assistant:

From a description:

I use imnot to mock external APIs for integration testing.
Generate a partner.yaml file in imnot format for [service name].
Schema reference: https://github.com/edu2105/imnot/blob/main/partners/README.md
Endpoints to mock: [describe them]
Output only the YAML — no code, no explanation.

From an OpenAPI spec:

I use imnot to mock external APIs for integration testing.
Convert this OpenAPI spec into a partner.yaml file in imnot format.
Schema: https://github.com/edu2105/imnot/blob/main/partners/README.md
Output only the YAML — no code, no explanation.
[paste your spec here]

Quick start

Requires Python 3.11 or later.

pipx install imnot
imnot init
imnot start

pipx installs CLI tools into isolated environments — no venv setup needed. Alternatively: pip install imnot inside an existing virtual environment.

imnot init creates a partners/ directory in the current folder with two working example partners (staylink and bookingco) that cover all the main patterns. Edit or replace them with your own YAML definitions whenever you're ready.

Expected output from imnot start:

Starting imnot on http://127.0.0.1:8000
INFO:     Started server process [12345]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

See what endpoints are available (no server needed):

imnot routes

How it works

  • Partner definitions live in partners/<name>/partner.yaml. Each file declares the partner's endpoints, the interaction pattern each endpoint follows, and the expected response shape.
  • Patterns capture common API interaction models:
    • oauth — client-credentials token endpoint that returns a static JWT.
    • static — endpoint that always returns a fixed JSON body defined in the YAML.
    • fetch — synchronous GET that returns the stored payload for a datapoint, with optional session isolation.
    • async — flexible N-step async flow defined in YAML: submit → optional status check(s) → fetch result.
    • push — imnot proactively delivers a payload to a callback URL after receiving a submit request.
    • paginated — offset/limit list endpoint that slices a stored array payload at request time.
  • Payload storage supports two modes:
    • Global — one payload per datapoint, last write wins.
    • Session — isolated payload per test run, selected via X-Imnot-Session header.
  • Admin API is always available at /imnot/admin/ for uploading payloads and inspecting sessions.

Interaction sequence (async pattern)

Test Harness                       imnot
     |                               |
     |  POST /admin/.../payload      |   (upload the response payload)
     |------------------------------>|
     |  200 OK                       |
     |<------------------------------|
     |                               |
     |  POST /partner/resource       |   step 1 — submit
     |------------------------------>|
     |  202 Accepted                 |
     |  Location: .../resource/{id}  |
     |<------------------------------|
     |                               |
     |  HEAD /partner/resource/{id}  |   step 2 — status check (optional)
     |------------------------------>|
     |  201  Status: COMPLETED       |
     |<------------------------------|
     |                               |
     |  GET  /partner/resource/{id}  |   step 3 — fetch result
     |------------------------------>|
     |  200  { ...payload }          |
     |<------------------------------|

The number and shape of steps is configurable per partner — 2-step, 3-step, and body-delivered IDs are all supported. See the async pattern documentation below.

Patterns

oauth

Returns a static JWT-shaped response. Use for standard OAuth 2.0 client-credentials token endpoints.

- name: token
  pattern: oauth
  endpoints:
    - method: POST
      path: /oauth/token
      response:
        status: 200
        token_type: Bearer
        expires_in: 3600

Response body:

{ "access_token": "<static-jwt>", "token_type": "Bearer", "expires_in": 3600 }

static

Returns whatever JSON body is defined under response.body in the YAML. Use for non-standard auth endpoints, health checks, or any fixed response.

Use static instead of oauth when the partner token endpoint returns custom fields that don't match the standard access_token / token_type / expires_in shape:

- name: token
  pattern: static
  endpoints:
    - method: POST
      path: /bookingco/auth/token
      response:
        status: 200
        body:
          token: "static-token-replace-in-real-use"
          my_custom_field: "some-value"

Static responses can be updated without restarting the server: edit the YAML, then call POST /imnot/admin/reload.

fetch

Single GET endpoint that returns the stored payload for the datapoint. Supports X-Imnot-Session for test isolation. Use for synchronous read endpoints.

- name: charges
  pattern: fetch
  endpoints:
    - method: GET
      path: /bookingco/v1/charges
      response:
        status: 200

Upload a payload first, then GET returns it:

curl -X POST http://localhost:8000/imnot/admin/bookingco/charges/payload \
     -H "Content-Type: application/json" \
     -d '{"charges": [{"id": "C1", "amount": 150}]}'

curl http://localhost:8000/bookingco/v1/charges

async

Flexible N-step async flow. Use for external APIs that submit work asynchronously and return the result via a separate endpoint. Step count and HTTP methods are fully configurable. Behavior is opt-in via two response flags:

  • generates_id: true — generate a UUID and deliver it via header or body field
  • returns_payload: true — return the stored payload for this datapoint

Header delivery (3 steps):

- name: reservation
  pattern: async
  endpoints:
    - step: 1
      method: POST
      path: /staylink/reservations
      response:
        status: 202
        generates_id: true
        id_header: Location
        id_header_value: /staylink/reservations/{id}
    - step: 2
      method: HEAD
      path: /staylink/reservations/{id}
      response:
        status: 201
        headers:
          Status: COMPLETED
    - step: 3
      method: GET
      path: /staylink/reservations/{id}
      response:
        status: 200
        returns_payload: true

Body delivery (separate status and results endpoints):

- name: rate-push
  pattern: async
  endpoints:
    - step: 1
      method: POST
      path: /ratesync/rates
      response:
        status: 200
        generates_id: true
        id_body_field: JobReferenceID
    - step: 2
      method: GET
      path: /ratesync/jobs/{id}/status
      response:
        status: 200
        body:
          status: COMPLETED
    - step: 3
      method: GET
      path: /ratesync/jobs/{id}/results
      response:
        status: 200
        returns_payload: true

push

imnot receives a submit request, returns immediately, then fires an outbound HTTP call to a callback URL with the stored payload — simulating the external service calling back your webhook endpoint.

Callback URL from request body field:

- name: rate-push
  pattern: push
  endpoints:
    - method: POST
      path: /partner/rates
      response:
        status: 202
        callback_url_field: callbackUrl     # body JSON field containing the callback URL
        callback_method: POST               # default: POST
        callback_delay_seconds: 0           # default: 0 (immediate)

Callback URL from request header:

- name: rate-push
  pattern: push
  endpoints:
    - method: POST
      path: /partner/rates
      response:
        status: 202
        callback_url_header: X-Callback-URL

Exactly one of callback_url_field or callback_url_header is required. The submit response body always includes a request_id (UUID) that can be used with the retrigger admin endpoint.

Interaction sequence:

Test Harness                    imnot                     Test Harness Webhook
     |                             |                               |
     |  POST /admin/.../payload    |                               |
     |---------------------------->|                               |
     |  POST /partner/rates        |                               |
     |  { "callbackUrl": "..." }   |                               |
     |---------------------------->|                               |
     |  202 { "request_id": "..." }|                               |
     |<----------------------------|                               |
     |                             |  POST http://.../webhook      |
     |                             |  { ...payload... }            |
     |                             |------------------------------>|

To re-fire the callback without restarting the flow:

curl -X POST http://localhost:8000/imnot/admin/{partner}/{datapoint}/push/{request_id}/retrigger

The retrigger always uses the current stored payload, so you can update the payload between attempts.

paginated

Upload a full array of items once; imnot slices the array at request time based on offset and limit query parameters. Use for list endpoints that consumers page through.

- name: listing
  pattern: paginated
  endpoints:
    - method: GET
      path: /ratesync/listings
      response:
        status: 200
  pagination:
    style: offset_limit      # v1 only value
    items_field: results     # required: key that holds the page in the response
    total_field: total       # optional: total dataset count
    has_more_field: hasMore  # optional: boolean — more pages exist
    next_offset_field: nextOffset  # optional: offset for the next page (null on last page)

Upload the dataset:

curl -X POST http://localhost:8000/imnot/admin/ratesync/listing/payload \
  -H "Content-Type: application/json" \
  -d '[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}]'

Fetch the first page:

curl "http://localhost:8000/ratesync/listings?offset=0&limit=2"
# → {"results":[{"id":1},{"id":2}],"total":5,"hasMore":true,"nextOffset":2}

The default page size (when limit is not sent) is configured via imnot.toml:

[pagination]
default_limit = 50

Session isolation (X-Imnot-Session) is supported — two sessions can hold different datasets and page through them independently.

For full schema documentation see partners/README.md.

Session-isolated testing

Any fetch, async, or paginated endpoint supports session isolation via X-Imnot-Session.

# Upload a session-scoped payload — returns a session_id
SESSION=$(curl -s -X POST http://localhost:8000/imnot/admin/bookingco/charges/payload/session \
               -H "Content-Type: application/json" \
               -d '{"charges": [{"id": "S1"}]}' | jq -r .session_id)

# Use the session in your request
curl http://localhost:8000/bookingco/v1/charges -H "X-Imnot-Session: $SESSION"

Multiple test users can run in parallel with isolated payloads — each gets their own session_id.

Admin endpoints

For every fetch, async, push, or paginated datapoint, imnot auto-generates payload endpoints:

Method Path Description
POST /imnot/admin/{partner}/{datapoint}/payload Upload global payload
GET /imnot/admin/{partner}/{datapoint}/payload Inspect current global payload
POST /imnot/admin/{partner}/{datapoint}/payload/session Upload session payload → returns session_id
GET /imnot/admin/{partner}/{datapoint}/payload/session/{session_id} Inspect a session payload
POST /imnot/admin/{partner}/{datapoint}/push/{request_id}/retrigger Re-fire callback for a prior push submit (push pattern only)

oauth and static datapoints do not get payload endpoints — their responses are fully defined by the YAML and never use the payload store.

Fixed infra endpoints (always available regardless of which partners are loaded):

Method Path Description
GET /healthz Health check — always returns {"status":"ok","version":"…"}, no auth required
GET /imnot/admin/partners List all loaded partners and their datapoints
POST /imnot/admin/partners Validate and register a new partner from a raw YAML body — routes go live immediately
GET /imnot/admin/sessions List all active sessions
POST /imnot/admin/reload Hot-reload partner YAMLs without restarting the server
GET /imnot/admin/postman Download a Postman collection v2.1 JSON for all loaded partners

POST /imnot/admin/partners accepts a raw YAML body (same format as partner.yaml files). Use ?force=true to overwrite an existing partner. Returns 201 on create, 200 on overwrite, 409 if the partner already exists without force, and 422 for invalid YAML.

Note — ephemeral storage: partners written at runtime are stored on the server's local filesystem. In a containerised deployment (Docker, Kubernetes), they will be lost if the pod restarts unless the partners directory is backed by a persistent volume. This is an infrastructure concern — imnot does not manage persistence.

Docs endpoints (public, no auth required):

Method Path Description
GET /imnot/docs Serve README.md as plain text
GET /imnot/docs/partners Serve partners/README.md as plain text

Securing admin endpoints

By default admin endpoints are open — suitable for local development only. When deploying on a shared network, protect them with a Bearer token:

# via environment variable (recommended)
IMNOT_ADMIN_KEY=your-secret-key imnot start

# or as a CLI flag
imnot start --admin-key your-secret-key

All /imnot/admin/* requests then require:

Authorization: Bearer your-secret-key

Consumer endpoints (/oauth/token, partner routes, etc.) are never affected. Set IMNOT_ADMIN_KEY in docker-compose.yml for Docker deployments.

CLI

Command Description
imnot init Scaffold a new project with example partner definitions and imnot.toml (optional — server works without it)
imnot init --dir <path> Scaffold into a specific directory (created if it does not exist)
imnot start Load partner YAMLs and start the server. Starts with zero partners if no partners/ dir exists. Auto-creates imnot.toml on first run.
imnot start --reload Start with auto-restart on any YAML change (recommended for development)
imnot stop Stop the running server by sending SIGTERM to the PID in imnot.pid
imnot stop --pid-file <path> Stop using an explicit PID file path
imnot generate --file <path> Validate and scaffold a partner YAML into partners/
imnot generate --file <path> --dry-run --json Validate only — print structured result, write nothing
imnot export postman Generate a Postman collection v2.1 JSON from all loaded partners
imnot export postman --out <file> Write the collection to a specific file (default: imnot-collection.json)
imnot export postman --partner <name> Include only the named partner (repeatable: --partner a --partner b)
imnot status Show active sessions in the store
imnot routes List all consumer and admin endpoints per partner (works from any subdirectory)
imnot payload get <partner> <datapoint> Print the current global payload
imnot payload set <partner> <datapoint> <file> Upload a global payload from a JSON file
imnot sessions clear Delete all sessions from the store

imnot start accepts --admin-key / IMNOT_ADMIN_KEY to protect all /imnot/admin/* endpoints with a Bearer token. CLI flags always override imnot.toml values.

Configuration

imnot.toml is created automatically by imnot start on first run. Edit it to persist settings without needing CLI flags every time.

[server]
# host = "127.0.0.1"                   # bind host
# port = 8000                          # bind port
# partners_dir = "partners"            # path to partner YAML directory
# db = "imnot.db"                      # path to SQLite database file
# base_url = "http://localhost:8000"   # used in generated Postman collections
# stop_timeout_seconds = 5             # seconds to wait for graceful shutdown

[logging]
# log_dir = "."                           # directory for log files
# max_bytes = 10485760                    # rotate when log file reaches this size (10 MB)
# backup_name_format = "date"             # "date" (2026-04-20) or "epoch" (1745789123)
# archived_logs_dir = "./archived-logs"   # rotated backups, relative to log_dir
# debug = false                           # enable DEBUG-level logs
# stdout = false                          # also emit to stdout (useful for Docker/ECS)

[pagination]
# default_limit = 50                      # default page size for paginated pattern endpoints

imnot writes two log files alongside imnot.db:

  • imnot.cli.log — CLI operations (start, stop, generate, etc.)
  • imnot.http.log — all HTTP traffic except /healthz

When a log file reaches max_bytes, it is moved to archived-logs/ with a timestamp in the filename and a new file starts. IMNOT_ADMIN_KEY is never written to logs.

Docker

Use Docker when you want to run imnot as a persistent background service — for example, on a shared dev server, in CI, or alongside other containers. For local development, the local install above is simpler.

A pre-built image is published at ghcr.io/edu2105/imnot:latest. To use it without building locally, set the image in docker-compose.yml:

image: ghcr.io/edu2105/imnot:latest

The partners/ directory and data/ (SQLite db) are volume-mounted — partners can be added without rebuilding the image, and state persists across restarts.

docker compose up                   # start
docker compose restart              # reload after adding a partner YAML
docker compose down                 # stop (data persists in ./data/)
docker compose down -v              # stop and wipe all state

The container binds to 127.0.0.1 by default. To expose it on the network, update docker-compose.yml and set an admin key:

ports:
  - "0.0.0.0:8000:8000"
environment:
  IMNOT_ADMIN_KEY: "your-secret-key"

Deploy to the cloud

The published Docker image (ghcr.io/edu2105/imnot) runs on any container platform. How you get that container running in your cloud is your domain — the specifics depend on your provider, infrastructure, and team setup. What imnot does require, regardless of where it runs:

  • Persistent storage — mount a volume at /app/data so the SQLite database survives container restarts. Without it, all session state is lost on redeploy.
  • Admin key — set IMNOT_ADMIN_KEY via environment variable. Required for any deployment reachable outside localhost.
  • Host binding — pass --host 0.0.0.0 as the start command so the container port is reachable from outside. The default 127.0.0.1 binding blocks external traffic.
  • Partner YAMLs — either commit them to the repo (included in the image build) or mount a volume at /app/partners to manage them independently.
  • Health checks — use GET /healthz for liveness and readiness probes. It always returns 200 {"status":"ok","version":"…"} with no auth and no I/O.

Adding a new partner

From the CLI (local / development)

Use imnot generate to validate and scaffold a partner YAML in one step — no need to know the directory layout or manually create files.

# Write your partner.yaml (see partners/README.md for the schema), then:
imnot generate --file /path/to/partner.yaml

# Dry-run first to validate without writing anything:
imnot generate --dry-run --file /path/to/partner.yaml --json

imnot generate validates the YAML, creates partners/<name>/ if needed, writes the file, and prints a summary of all consumer and admin endpoints.

With --reload (recommended for development): the server picks up the new file and restarts automatically.

Without --reload: call POST /imnot/admin/reload or restart imnot start.

Over HTTP (containerised deployment)

When imnot runs as a pod or container, use POST /imnot/admin/partners to register a new partner without exec-ing into the container. The endpoint validates the YAML, writes it to the partners directory, and registers its routes immediately — no restart required.

curl -X POST http://localhost:8000/imnot/admin/partners \
     -H "Authorization: Bearer $IMNOT_ADMIN_KEY" \
     --data-binary @/path/to/partner.yaml

# Overwrite an existing partner definition:
curl -X POST "http://localhost:8000/imnot/admin/partners?force=true" \
     -H "Authorization: Bearer $IMNOT_ADMIN_KEY" \
     --data-binary @/path/to/partner.yaml

imnot generate and POST /imnot/admin/partners use identical validation — both write the same partners/<name>/partner.yaml file and produce the same JSON result shape.

Persistence reminder: partners written at runtime live on the container's local filesystem. Mount a persistent volume at /app/partners if you need them to survive restarts.

You can also write the YAML manually. Either way, the directory structure is:

partners/
└── mypartner/
    ├── partner.yaml
    └── payloads/       # optional example payload files

Project structure

imnot/
├── imnot/
│   ├── api/           # FastAPI app factory
│   ├── engine/
│   │   ├── patterns/  # oauth / static / fetch / async / push / paginated handlers
│   │   ├── router.py  # dynamic route registration
│   │   └── session_store.py  # SQLite persistence
│   ├── loader/        # YAML partner definition parser
│   ├── partners.py    # register_partner() — shared by CLI and HTTP admin endpoint
│   └── cli.py         # imnot CLI
├── partners/
│   ├── staylink/      # StayLink example (oauth + async)
│   │   ├── partner.yaml
│   │   └── payloads/
│   └── bookingco/     # BookingCo example (static token + fetch charges)
│       └── partner.yaml
└── tests/

Limitations & Roadmap

  • push callbacks have no retry logic — if the callback URL is unreachable, the failure is logged and the retrigger endpoint can be used to re-fire.
  • No native HTTPS support — use a reverse proxy (Nginx, Caddy) to terminate TLS.
  • No web UI — all admin interactions are via the REST API or CLI.
  • XML response bodies are not supported — responses are always JSON.
  • No built-in mTLS support.
  • Single-node only — the SQLite session store is not shared across instances.

Contributing

Run the unit + integration test suite:

git clone https://github.com/edu2105/imnot.git
cd imnot
pip install -e ".[dev]"
pytest

Run the end-to-end QA suite (requires a local imnot install):

./scripts/qa/run.sh

The QA suite spins up a real server in a fresh temp directory and runs five phases:

  1. CLI commands (imnot init, routes, export postman, generate)
  2. Full pattern flows for the default partners (staylink + bookingco)
  3. imnot generate testingpartner covering all six patterns + hot reload
  4. Endpoint flows for the generated partner (oauth, async, static, fetch, push, paginated)
  5. Stop / restart / DB persistence check

Run this after any change that could affect the public contract: YAML schema, CLI interface, pattern behaviour, or the session store.

Add a new pattern: Patterns live in imnot/engine/patterns/. Each pattern is a module that registers one or more FastAPI route handlers given an EndpointDef. Look at fetch.py or async_.py for the interface — the router calls the pattern's handler factory for each datapoint whose pattern matches. Add your module there and wire it into router.py.

Add a new partner: No code required — write a partner.yaml under partners/<name>/. The full schema and field reference is in partners/README.md.

Looking for where to start? Open issues are tracked at github.com/edu2105/imnot/issues.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

imnot-0.5.2.tar.gz (3.5 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

imnot-0.5.2-py3-none-any.whl (47.4 kB view details)

Uploaded Python 3

File details

Details for the file imnot-0.5.2.tar.gz.

File metadata

  • Download URL: imnot-0.5.2.tar.gz
  • Upload date:
  • Size: 3.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for imnot-0.5.2.tar.gz
Algorithm Hash digest
SHA256 101c38b4f0230fb0144978c7613e35907d0fe034d377ad34791389f8d4b24143
MD5 7ada3a58302a00353cda7618683b4101
BLAKE2b-256 048b83d44e7c1d923d3fbfab8891e84e990ba6a092e1bd60a5a3314517a8b9ee

See more details on using hashes here.

Provenance

The following attestation bundles were made for imnot-0.5.2.tar.gz:

Publisher: release.yml on edu2105/imnot

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file imnot-0.5.2-py3-none-any.whl.

File metadata

  • Download URL: imnot-0.5.2-py3-none-any.whl
  • Upload date:
  • Size: 47.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for imnot-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 87b7d5982f7b1778d9cabe39aaddec43e22c57d6e0a1bbb9f84b769a45748a00
MD5 d2e5cd10566725433fe7428fbc7b0d2e
BLAKE2b-256 05e6686e2dc4b7c8bc1abf004efd430447ea25c57f665cd949b380e924ff6b8c

See more details on using hashes here.

Provenance

The following attestation bundles were made for imnot-0.5.2-py3-none-any.whl:

Publisher: release.yml on edu2105/imnot

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page