A stateful API mock server for integration testing
Project description
imnot
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 start
pipxinstalls CLI tools into isolated environments — no venv setup needed. Alternatively:pip install imnotinside an existing virtual environment.
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-Sessionheader.
- 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 fieldreturns_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/dataso the SQLite database survives container restarts. Without it, all session state is lost on redeploy. - Admin key — set
IMNOT_ADMIN_KEYvia environment variable. Required for any deployment reachable outside localhost. - Host binding — pass
--host 0.0.0.0as the start command so the container port is reachable from outside. The default127.0.0.1binding blocks external traffic. - Partner YAMLs — either commit them to the repo (included in the image build)
or mount a volume at
/app/partnersto manage them independently. - Health checks — use
GET /healthzfor liveness and readiness probes. It always returns200 {"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/partnersif 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
pushcallbacks 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:
git clone https://github.com/edu2105/imnot.git
cd imnot
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file imnot-0.4.2.tar.gz.
File metadata
- Download URL: imnot-0.4.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.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41a1a669e3f4d50da99c1b8c28cca4cda2824d888c784848ceb48c22ecc3c814
|
|
| MD5 |
7319a3513b2892e07aaa7b0344efb935
|
|
| BLAKE2b-256 |
482cf8b905b3322de96dfcbaf544699b89654547f92a17f58f09852648523229
|
Provenance
The following attestation bundles were made for imnot-0.4.2.tar.gz:
Publisher:
publish-pypi.yml on edu2105/imnot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
imnot-0.4.2.tar.gz -
Subject digest:
41a1a669e3f4d50da99c1b8c28cca4cda2824d888c784848ceb48c22ecc3c814 - Sigstore transparency entry: 1316990364
- Sigstore integration time:
-
Permalink:
edu2105/imnot@49e45e3a298966e87e47346e27a194f352488622 -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/edu2105
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@49e45e3a298966e87e47346e27a194f352488622 -
Trigger Event:
push
-
Statement type:
File details
Details for the file imnot-0.4.2-py3-none-any.whl.
File metadata
- Download URL: imnot-0.4.2-py3-none-any.whl
- Upload date:
- Size: 36.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
983b0d026d60c9df70153e9981d656f4d530b15d5b6032834050252fdb35310e
|
|
| MD5 |
98961daf747d5854575c609add3a0850
|
|
| BLAKE2b-256 |
ff8a03a74e62cf0fda349f9af96137deb945159381376e0ee0f444bfc2a9c493
|
Provenance
The following attestation bundles were made for imnot-0.4.2-py3-none-any.whl:
Publisher:
publish-pypi.yml on edu2105/imnot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
imnot-0.4.2-py3-none-any.whl -
Subject digest:
983b0d026d60c9df70153e9981d656f4d530b15d5b6032834050252fdb35310e - Sigstore transparency entry: 1316990379
- Sigstore integration time:
-
Permalink:
edu2105/imnot@49e45e3a298966e87e47346e27a194f352488622 -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/edu2105
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@49e45e3a298966e87e47346e27a194f352488622 -
Trigger Event:
push
-
Statement type: