Skip to main content

A stateful API mock server for integration testing

Project description

imnot

imnot — stateful API mock server

CI

imnot is a stateful API mock server for integration testing.

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.

git clone https://github.com/edu2105/imnot.git
cd imnot
python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e .
imnot start

Expected output:

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.
  • 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.

Session-isolated testing

Any fetch or async 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, or push 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 start Load all partner YAMLs and start the server
imnot start --reload Start with auto-restart on any YAML change (recommended for development)
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.

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 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 test suite:

pip install -e ".[dev]"
pytest

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.4.1.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.4.1-py3-none-any.whl (36.2 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for imnot-0.4.1.tar.gz
Algorithm Hash digest
SHA256 3a47ea8bf5f74421bab5fb32b4d69ec42493878ef972aa3b8e644ca28fccf6b2
MD5 a6491c2e87574bdd1caf66b1a12a5411
BLAKE2b-256 7f6772e8c3ef107b5be4007068d46c58120ae8b063040a81d0f5d2ea3d413a55

See more details on using hashes here.

Provenance

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

Publisher: publish-pypi.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.4.1-py3-none-any.whl.

File metadata

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

File hashes

Hashes for imnot-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e6f1cb97ca9c1d6bf99a38ef11f193db911f49c987b16637aceb3b19920837d6
MD5 34d1bd1e5dab73cd7575a4aff85a26f0
BLAKE2b-256 bae78c2cace42ca1d81810cf5267fca576af457a5d7a2e0e023c5df077becbe4

See more details on using hashes here.

Provenance

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

Publisher: publish-pypi.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