SnapAPI
SnapAPI is a small language for HTTP tests. Write a .sapi file, run snapapi, get PASS or FAIL.
It is not a replacement for every API framework. If your tests are mostly request → assert → extract → request → assert, that's the 80% SnapAPI is for. Loops, custom crypto inside the suite, WebSockets, gRPC, or importing 2,000 existing cases: keep the tool you have. Need more power? CALL a Python extension and bring the result back — don't grow the test language. There is no IF / FOR / WHILE in the file — when SnapAPI is not for you.
Start here: playground (no install) → quick start (same GET on the CLI).
The PyPI package is pysnapapi. The command is snapapi. pip install snapapi is a different project.
First PASS
You need something that answers GET /users. snapapi mock is a tiny server for that — local, not the public internet.
Create mock.json:
{
"routes": [
{
"method": "GET",
"path": "/users",
"status": 200,
"json": { "data": [{ "id": 1, "name": "Ada" }] }
}
]
}
Create hello.sapi:
SUITE: Hello API
URL: http://127.0.0.1:8765
TEST: Get Users
GET: /users
EXPECT: status == 200
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install pysnapapi
snapapi --version
snapapi mock mock.json --port 8765 # leave running
snapapi hello.sapi
You want 1 passed 0 failed. Same files in the repo: examples/hello/mock.json and examples/hello/hello.sapi.
After first PASS
Guide pages, in order:
- POST + BODY
- SAVE an id, then
${userId} - Env / AUTH
- HELPER / SETUP
- Your API — put your real origin in
URL:
When SnapAPI is not for you · CALL (extensions/) · Troubleshooting if you’re stuck. Flag tables below are reference.
Features
- Human-readable DSL for GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS
- Attach
BODY/DATA,HEADERs,QUERY/PARAM, andAUTHto the current request EXPECTchecks: status, body contains, JSONPath (filters + collection asserts), XPath, response headersSAVEvalues from JSON responses and reuse them as${var}- Setup / teardown with cycle detection
- Env files, tag filters, timeouts, retries, JSON and JUnit reports
- OpenAPI response/request contract checks, VCR cassettes, JSON mock server
- pytest plugin (
snapapi_run/@pytest.mark.snapapi) - Python
CALL/extensions/for values the HTTP DSL will not grow keywords for - VS Code syntax highlighting and diagnostics for
.sapifiles
Requirements
- Python 3.9 or newer
Installation
From PyPI (most people):
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install pysnapapi
snapapi --version
From this repository (contributors):
git clone https://github.com/deekshith-poojary98/snapapi.git
cd snapapi
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
Or with the pinned runtime dependencies:
pip install -r requirements.txt
pip install -e .
CLI
After a green hello. You don’t need this table to get a first PASS.
snapapi path/to/file.sapi
python -m snapapi path/to/file.sapi
Pass multiple files or a directory of .sapi files:
snapapi examples/hello/hello.sapi
snapapi examples/users/post.sapi
snapapi tests/
Options:
| Flag | Meaning |
|---|---|
-k "Login or not Health" |
Pytest-style keyword expression on name/description/tags |
-m "smoke and not slow" |
Pytest-style tag expression |
--tag user |
Run tests that have this tag (repeatable; all given tags must match) |
--exclude slow |
Skip tests with this tag (repeatable) |
--name "Create User" |
Run tests with this name (repeatable) |
--env .env |
Load KEY=VALUE pairs for ${VAR}. If omitted, SnapAPI loads <suite>.env, then .env next to the file, then a single sibling *.env |
-D TOKEN=secret |
Set ${VAR} from the CLI (repeatable) |
--timeout 10 |
HTTP timeout in seconds (overrides TIMEOUT) |
--report json:report.json |
Write a JSON report |
--report junit:report.xml |
Write a JUnit XML report |
--report html:report.html |
Write a self-contained HTML report |
-x / --exitfirst / --stop-on-failure |
Stop each suite on the first failed test (off by default) |
--maxfail N |
Stop after N failures |
--collect-only |
List selected tests without HTTP |
-q / -v |
Quiet or verbose console |
--durations N |
Show the N slowest tests |
--profile stage |
Load environments/stage.env, .snapapi/stage.env, or stage.env |
--workers N |
Run independent tests in parallel (SAVE is isolated per test; sibling SAVE falls back to sequential) |
--grep regex |
Filter tests by name/description |
--lf / --last-failed |
Re-run failures from .snapapi/last-run.json (matches file + suite + name, including Test [row]) |
--ff / --failed-first |
Run last-failed tests first, then the rest |
--mode record|replay|record-on-miss |
VCR cassettes under .snapapi/cassettes/ |
--record-on-miss |
With --mode replay, hit the network and save when a cassette is missing |
--vcr-match query,body,accept,authorization |
Cassette identity fields (default: query, content-type, accept, body) |
--contract-strict |
Fail when an OpenAPI path/method/schema is missing (default: skip/warn) |
--reruns N |
Re-run failed tests up to N times (distinct from EXPECT RETRY) |
--listener PATH[:Class] |
Python listener called after each test / suite (repeatable) |
--plugin PATH[:name] |
Extra Python extension (repeatable). Prefer extensions/ or snapapi.yaml |
--on-fail curl / --on-fail har:dir |
Emit a redacted curl or HAR on failure |
--safe-url |
Block private/metadata hosts |
--proxy URL |
HTTP/HTTPS proxy |
--insecure |
Skip TLS certificate verification |
--cert PATH |
Client certificate |
--cacert PATH |
CA bundle used to verify TLS |
snapapi lint PATH |
Parse/validate without HTTP |
snapapi fmt PATH |
Format .sapi files |
snapapi convert 'curl …' / file.sh / --clipboard |
Convert curl to a .sapi suite |
snapapi openapi spec.yaml |
Generate GET/POST/PUT/PATCH/DELETE smoke tests |
snapapi history [--failed] [--since 7d] |
Print .snapapi/history.jsonl |
snapapi mock mock.json [--port 0] |
Serve routes from a JSON mock file (prints the URL) |
snapapi watch PATH [--interval 0.5] |
Re-run when .sapi files change (poll; optional watchdog extra) |
The process exits 0 when every test passed, 1 when a test failed, and 2 on parse or usage errors.
DSL
Recommended form (after you have a first PASS — see above). https://api.example.com is a placeholder, not a live host:
SUITE: Book Store
DESC: Validates the user API
TIMEOUT: 10
URL: https://api.example.com
HEADER Content-Type: application/json
TEST: Create User
DESC: Create a user and keep the id
TAG: users write
POST: /users
AUTH: bearer ${TOKEN}
BODY: {"name": "Jane", "email": "jane@example.com"}
EXPECT: status == 201
EXPECT: body contains id
SAVE: userId FROM $.id
TEST: Get User
TAG: users
SETUP: Create User
GET: /users/${userId}
EXPECT: status == 200
EXPECT: json $.email == "jane@example.com"
EXPECT: header Content-Type contains json
TEST: List Users
GET: /users
QUERY: page=2&limit=10
PARAM: sort name
EXPECT: status == 200
TEST: Wait for ready
GET: /jobs/${id}
WAIT: json $.status == "ready" TIMEOUT 10s BACKOFF 0.5s
EXPECT: status == 200
Comments are // lines. Indentation is cosmetic. JSON bodies may be one line or span multiple lines.
The older forms still parse:
OPTIONS: {"TIMEOUT": 10}
HEADERS: {"Content-Type": "application/json"}
TAG: users, write
REQUEST: POST /users
HEADERS: {"Authorization": "Bearer ${TOKEN}"}
DATA: {"name": "Jane"}
EXPECT: STATUS 201
EXPECT: CONTAINS id
EXPECT: JSON $.email == "jane@example.com"
EXPECT: HEADER Content-Type CONTAINS json
Keywords
- Suite:
SUITE,DESC,URL,TIMEOUT,FOLLOW-REDIRECTS,OPTIONS,IMPORT,SUITE-SETUP,SET,CALL - Test:
TEST,TAG,SETUP,TEARDOWN,DEPENDS,SKIP,ONLY,QUARANTINE,EXAMPLES,SET,CALL - Helper:
HELPER(named procedure forSUITE-SETUP/SETUP/TEARDOWN; not a test case) - Request:
REQUEST,GET/POST/PUT/PATCH/DELETE/HEAD,BODY/DATA,FILE,GRAPHQL,HEADER/HEADERS,QUERY,PARAM,AUTH,EXPECT,SAVE,WAIT,SET,CALL
HTTP OPTIONS is written as REQUEST: OPTIONS /path so it does not collide with suite-level OPTIONS: {...} JSON. HEAD: /x is a request alias like GET:.
SETUP / TEARDOWN name a HELPER or TEST. Prefer HELPER: for procedures that should not appear as cases. A TEST named only as setup is still treated as a helper (legacy).
DEPENDS: Create User (comma-separated for several names) keeps both tests as primaries. SnapAPI reorders so named tests run first; unrelated tests keep file order. If a named test failed, skipped, or was not selected (for example -k), the dependent test is skipped with that reason. Cycles and unknown names are parse errors. DEPENDS cannot target a HELPER. This is not SETUP: — setup helpers always run first and fail the dependent test when they fail.
IMPORT: other.sapi pulls tests from another file (paths are relative to the current file).
SET: orderId ${uuid()} assigns an interpolated value (including helpers) without an HTTP call. It may appear at suite, test, or step level.
WAIT: json $.status == "ready" TIMEOUT 10s BACKOFF 0.5s reissues the current request until the check passes or the timeout expires. EXPECT: json $.status == "ready" RETRY 20 BACKOFF 0.5s also retries when the HTTP status is already 200.
AUTH: bearer ${TOKEN} sets Authorization: Bearer ${TOKEN}. Explicit HEADER lines still work.
AUTH: oauth2 grant=client_credentials token_url=... client_id=... and grant=password username=... password=... fetch a token (cached). If the token response includes refresh_token, a 401 retries once after refresh.
AUTH: oauth2 grant=authorization_code token_url=... client_id=... redirect_uri=... code=${AUTH_CODE} pkce=true code_verifier=${PKCE_VERIFIER} exchanges an authorization code. SnapAPI does not open a browser or perform the authorize redirect; supply ${AUTH_CODE} and, for PKCE, the same ${PKCE_VERIFIER} you used when deriving code_challenge for the authorization request. With pkce=true the token request sends only code_verifier (RFC 7636) — never a freshly generated challenge.
AUTH: digest user:pass uses requests HTTP Digest Auth.
QUERY: page=2&limit=10 and PARAM: page 2 attach query parameters to the current request (they merge with any query string already in the path).
OPTIONS: {"OPENAPI": "spec.yaml"} validates JSON responses (and request bodies/required params) against the matching path+method schema when present. Missing path/schema is skipped by default. Strict mode fails instead:
OPTIONS: {"OPENAPI": "spec.yaml", "OPENAPI-STRICT": true}
EXPECT: openapi ./spec.yaml strict
CLI --contract-strict is the same switch. Partial path match (/users/{id} vs /users/1) is allowed.
Checks
Check types are case-insensitive. Preferred:
EXPECT: status == 200
EXPECT: status != 500
EXPECT: status == 200 RETRY 5 ON 5xx BACKOFF 1s
EXPECT: body contains userId
EXPECT: body not contains stack
EXPECT: body matches "userId":
EXPECT: body not matches stack.?trace
EXPECT: body empty
EXPECT: body not empty
EXPECT: body starts-with {"ok"
EXPECT: body ends-with }
EXPECT: json $.email == "jane@example.com"
EXPECT: json $.count != 0
EXPECT: json $.count > 0
EXPECT: json $.email matches ^.+@example\\.com$
EXPECT: json $.email not matches @tempmail
EXPECT: json $.email equals-ignoring-case "Jane@Example.com"
EXPECT: json $.email contains-ignoring-case "@example"
EXPECT: json $.items length == 3
EXPECT: json $.items empty
EXPECT: json $.items not empty
EXPECT: json $.password absent
EXPECT: json $.id exists
EXPECT: json $.id present
EXPECT: json $.secret not exists
EXPECT: json $.id type string
EXPECT: json $.status in ["open","pending"]
EXPECT: json $.status not in ["error","failed"]
EXPECT: json $.email starts-with "ada@"
EXPECT: json $.email ends-with "@example.com"
EXPECT: json $.tags contains "a"
EXPECT: json $.tags not contains "z"
EXPECT: json $.tags contains-all ["a","b"]
EXPECT: json $.tags contains-only ["a","b"]
EXPECT: json $.tags contains-any ["admin","owner"]
EXPECT: json $.events contains-sequence ["created","paid"]
EXPECT: json $.roles subset-of ["admin","editor","viewer"]
EXPECT: json $ contains-keys ["id","email"]
EXPECT: json $ not contains-keys ["password"]
EXPECT: json $.ids unique
EXPECT: json $.ids sorted
EXPECT: json $.ids sorted desc
EXPECT: json $.count between 1 10
EXPECT: json $.score close-to 0.33 delta 0.01
EXPECT: json $.count positive
EXPECT: json $.balance zero
EXPECT: json $.debt negative
EXPECT: json $.items[*].id contains 3
EXPECT: json $.items[?(@.status=="open")].id contains 3
EXPECT: json $.items each $.status == "active"
EXPECT: json $.ok == true BECAUSE "login should succeed"
EXPECT: status == 400 OR status == 401
EXPECT: json $.success == false AND body contains error
EXPECT: (status == 400 OR status == 401) AND json $.success == false
EXPECT: schema ./schemas/user.json
EXPECT: schema inline {"type":"object"}
EXPECT: duration < 200ms
EXPECT: header Content-Type == application/json
EXPECT: header Content-Type != text/html
EXPECT: header Content-Type contains json
EXPECT: header Content-Type not contains html
EXPECT: header Content-Type matches json
EXPECT: header Content-Type starts-with application
EXPECT: header Content-Type ends-with json
EXPECT: header Content-Type equals-ignoring-case APPLICATION/JSON
EXPECT: header Content-Type contains-ignoring-case json
EXPECT: header X-Env in ["stage","prod"]
EXPECT: header X-Env not in ["dev"]
EXPECT: header X-Empty empty
EXPECT: header Authorization not empty
EXPECT: header X-Debug absent
EXPECT: header Authorization exists
EXPECT: header Authorization present
EXPECT: header Set-Cookie not contains session
EXPECT: openapi ./openapi.yaml
EXPECT: openapi ./openapi.yaml strict
EXPECT: xpath //Order/@id == "1"
Catalog (every operator): DSL → Expect.
Also accepted:
EXPECT: STATUS 200
EXPECT: CONTAINS userId
EXPECT: JSON $.data.email == "jane@example.com"
EXPECT: HEADER Content-Type CONTAINS json
JSONPath is a small subset: $.a.b, $.items.0.id, $.items[0].id, $.items[*].id, and equality filters $.items[?(@.status=="open")] / $.items[?(@.id==1)].
AND / OR combine checks on one line (AND binds tighter than OR; parentheses group). Quote a value if it contains those words. Multiple EXPECT lines on the same request all run; if more than one fails, every failure is reported together. BECAUSE "reason" on an EXPECT line is prepended to that check's failure message.
XPath uses stdlib xml.etree (descendant tags and /@attr). Axes, namespaces, and functions are not implemented.
Variables
${NAME} is expanded in URLs, paths, headers, data, and expect values.
Lookup order: process environment, then --env / auto-discovered suite env file, then SET / SAVE / CALL values.
Built-in functions: ${uuid()}, ${now()}, ${random.int(min,max)}. Custom functions use CALL: signature = crypto.generate_signature(${PAYLOAD}, ${SECRET}) with modules in extensions/. They take explicit arguments and cannot skip steps or make HTTP.
Sample suite
tests/recommended.snaptest shows the current DSL against a local mock server (pytest injects BASE_URL). tests/test_suite.snaptest is a classic-syntax example against reqres.in and needs network access. Automated tests in tests/test_*.py use a local mock HTTP server and do not call reqres. CI replays tests/fixtures/offline.snaptest from a checked-in cassette.
HTML reports include redacted request/response bodies. VCR cassette keys include method, path, and (by default) sorted query string, Content-Type/Accept, and body. OPTIONS: {"VCR-MATCH": ["query","body","accept","authorization"]} or --vcr-match authorization,query replaces that default. Replay restores Set-Cookie onto the session.
snapapi mock tests/fixtures/mock.json --port 0 serves JSON routes. Routes may use path templates (/users/{id}), optional match.query / match.body subsets, and delay_ms. Exact paths win over templates. There is no language server, gRPC, or WebSocket support. Python plugins are CLI-only (not the playground).
pytest plugin
Install with pip install -e ".[dev]". Then:
def test_suite(snapapi_run):
result = snapapi_run("tests/foo.sapi")
assert result.ok
@pytest.mark.snapapi("tests/foo.sapi")
def test_marked(snapapi_run, request):
snapapi_run(request.node.get_closest_marker("snapapi").args[0])
snapapi_run(path, **engine_kwargs) returns SuiteResult and fails the pytest case when the suite is not ok. Pass plugins={"hmac": hmac} if you are not using extensions/.
Project layout
snapapi/
├── snapapi/ # Python package
│ ├── parser.py # .sapi DSL parser
│ ├── engine.py # runner, checks, setup/teardown
│ ├── api_client.py # requests wrapper
│ └── cli.py # snapapi command
├── tests/ # pytest + sample .sapi / .snaptest suites
├── examples/ # hello GET, users POST/SAVE/AUTH/HELPER, plugins
├── docs/ # User guide + in-browser playground
├── snapapi-language/ # VS Code grammar / run command
├── pyproject.toml
└── README.md
Running the tests
pytest
VS Code
The snapapi-language extension is a language pack for .sapi files: syntax highlighting, snippets, completions, lightweight diagnostics (unknown keywords, unknown SETUP names, HEAD: / REQUEST: OPTIONS), and SnapAPI: Run current file / Run test at cursor. See snapapi-language/README.md. There is no separate language-server process.
License
MIT — see LICENSE.
Metadata
Release files for pysnapapi 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pysnapapi-0.5.0.tar.gz | 130.7 kB | Without provenance | Details for pysnapapi-0.5.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pysnapapi-0.5.0-py3-none-any.whl | Python 3 | none | any | Details for pysnapapi-0.5.0-py3-none-any.whl |
Total release size: 228.2 kB
Release files / pysnapapi-0.5.0.tar.gz
| Download URL | pysnapapi-0.5.0.tar.gz |
|---|---|
| Size | 130.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2de3597a727e4c05bdda74918e8ffd171cc2399c4d33e4574dcd0068640ce05c
|
|
BLAKE2b-256 checksum How to use checksums |
26c802947bfda8047fc6542466784b6a2f7ff7a583e1515f21197077734bed21
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|
Release files / pysnapapi-0.5.0-py3-none-any.whl
| Download URL | pysnapapi-0.5.0-py3-none-any.whl |
|---|---|
| Size | 97.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5bd110cf5edba0940a72a7c35502d3cce576946119009655720165c6e0bcefd8
|
|
BLAKE2b-256 checksum How to use checksums |
5111750ae90df3be46ddd843534ccb53133dd206281ad18946f48cbc59f99f22
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|
Security for pysnapapi 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.