Language
DSL reference
What the parser accepts. Indentation doesn’t matter. New files should be .sapi; older .snaptest fixtures still run.
Suite
SUITE: Book Store
DESC: Validates the user API
TIMEOUT: 10
FOLLOW-REDIRECTS: true
URL: https://api.example.com
HEADER Content-Type: application/json
HELPER: Login
POST: /login
BODY: {"email": "${EMAIL}", "password": "${PASSWORD}"}
EXPECT: status == 200
SAVE: token FROM $.token
SUITE-SETUP: Login
IMPORT: shared.sapi
| Keyword | Meaning |
|---|---|
SUITE | Display name. Defaults to the file stem if omitted. |
DESC | Suite description, or test description when it sits under TEST. |
URL | Base URL. Can be set per test. Interpolates ${VAR}. https://api.example.com in samples is a placeholder — see Your API. |
TIMEOUT | HTTP timeout in seconds for the suite. |
FOLLOW-REDIRECTS | Boolean. Also allowed on a step. |
OPTIONS | JSON object of suite options (TIMEOUT, OPENAPI, VCR-MATCH, …). Suite-level only; HTTP OPTIONS uses REQUEST: OPTIONS /path. Stop-on-failure is CLI-only: -x / --stop-on-failure. |
HEADER / HEADERS | Default headers. Line form or JSON object. |
AUTH | Default auth for following requests. |
SET | Assign a value without HTTP, e.g. SET: orderId ${uuid()}. |
CALL | CALL: signature = crypto.generate_signature(${PAYLOAD}, ${SECRET}). Stores the return value. See CALL. |
IMPORT | Pull tests from another .sapi file (path relative to the current file). Not in the playground. |
HELPER | Named procedure with the same request/EXPECT/SAVE body as a test, but it is not a test case. Use it from SUITE-SETUP / SETUP / TEARDOWN. |
SUITE-SETUP | Name of a HELPER (or a TEST) to run once before primaries. Failure skips primaries and fails the suite. SUITE-TEARDOWN still runs afterward when declared. |
SUITE-TEARDOWN | Runs once after primaries (or after a failed SUITE-SETUP). Failure fails the suite and is never discarded, even if tests already failed. |
Test
TEST: Get User
DESC: Fetch one user
TAG: users read
SETUP: Create User
TEARDOWN: Cleanup User
EXAMPLES:
name,email
Ada,ada@example.com
Bob,bob@example.com
GET: /users/${userId}
| Keyword | Meaning |
|---|---|
TEST | A case that runs and is counted. Names referenced by SETUP/TEARDOWN/DEPENDS must exist. |
TAG | Space- or comma-separated tags. CLI --tag requires all given tags to match. |
SETUP / TEARDOWN | Name a HELPER or TEST. Those procedures are not counted as cases. Cycles are parse errors. |
DEPENDS | Name one or more primary tests (comma-separated). SnapAPI runs those tests first (file order otherwise). If any failed, skipped, or was not selected, this test is skipped with that reason. Cannot target a HELPER. |
EXAMPLES | Inline CSV (header + rows) or a .csv path. Each row becomes Test [first-cell] with those variables. Playground supports inline tables only. |
SKIP / ONLY / QUARANTINE | CLI filters. Not implemented in the playground (you get a clear error). |
SET | Test-scoped assignment, interpolated before steps. |
CALL | Test-scoped CALL: name = function(${ARG}). Not in the playground. |
Request
Preferred aliases: GET:, POST:, PUT:, PATCH:, DELETE:, HEAD: plus a path. HTTP OPTIONS must be written as REQUEST: OPTIONS /path so it does not collide with suite OPTIONS: {...}.
TEST: List Users
GET: /users
QUERY: page=2&limit=10
PARAM: sort name
HEADER Accept: application/json
AUTH: bearer ${TOKEN}
EXPECT: status == 200
| Keyword | Meaning |
|---|---|
REQUEST: METHOD /path | Legacy / OPTIONS form. Method is one of GET POST PUT PATCH DELETE HEAD OPTIONS. |
QUERY | page=2&limit=10 merged onto the path query string. |
PARAM | PARAM: page 2 or PARAM: page=2. |
HEADER / HEADERS | Step headers overlay test headers overlay suite headers. |
AUTH: bearer TOKEN | Sets Authorization: Bearer TOKEN. Also token, basic user:pass. |
AUTH: digest user:pass | HTTP Digest (CLI / requests). Not in the playground. |
AUTH: oauth2 … | grant=client_credentials, password, or authorization_code with token_url and client_id. Tokens are cached; 401 retries once after refresh. SnapAPI does not open a browser for the code grant. With pkce=true, also pass code_verifier (the same verifier used to create the authorize code_challenge); the token request sends only that verifier. Not in the playground. |
Bodies
POST: /users
BODY: {
"name": "Jane",
"email": "jane@example.com"
}
POST: /login
BODY: form user=jane&pass=secret
POST: /ingest
BODY: raw text/plain hello
POST: /upload
FILE: avatar FROM ./photo.png
POST: /graphql
GRAPHQL: {"query": "{ user(id: 1) { email } }"}
BODY and DATA are aliases. JSON may be one line or span following lines until the value is complete. FILE and GRAPHQL are CLI-only; the playground reports them as unsupported.
Expect
Check kinds are case-insensitive. Preferred forms use lowercase kinds (json, header, body, status).
EXPECT: status == 200
EXPECT: json $.ok == true BECAUSE "login should succeed"
EXPECT: status == 400 OR status == 401
EXPECT: (status == 400 OR status == 401) AND json $.success == false
EXPECT: status == 200 RETRY 5 ON 5xx BACKOFF 1s
AND / OR combine checks on one line (AND binds tighter). Multiple EXPECT lines all run; failures are reported together. BECAUSE "reason" prefixes that check’s failure message.
Status
| Form | Meaning |
|---|---|
status == 200 / != 500 | HTTP status equals / not equals |
status == 200 RETRY N … | Retry when status is retryable (see WAIT) |
Body
| Form | Meaning |
|---|---|
body contains text / not contains | Substring present / absent |
body matches regex / not matches | Regex search |
body starts-with / ends-with | Prefix / suffix |
body empty / not empty | Empty or non-empty response text |
JSON
JSONPath subset: $.a.b, $.items[0].id, $.items[*].id, $.items[?(@.status=="open")].
| Form | Meaning |
|---|---|
json $.x == v / != / > >= < <= | Compare value. ==/!= use JSON types (same as the playground): true != 1, false != 0; numbers compare numerically so 1 == 1.0. |
json $.x contains v / not contains | Array membership or string substring |
json $.x matches / not matches | Regex on string value |
json $.x starts-with / ends-with | String prefix / suffix |
json $.x equals-ignoring-case | Case-insensitive equality |
json $.x contains-ignoring-case | Case-insensitive substring |
json $.x in […] / not in […] | Value in / not in a list |
json $.x length == N (also != > …) | Length of array/string/object |
json $.x empty / not empty | Empty container or string |
json $.x absent / not exists | Zero matching paths (null still counts as present). With [*], every element must lack the field. |
json $.x exists / present | At least one matching path resolves. With [*], any element having the field is enough. |
json $.x type string|number|array|object|boolean|null | JSON type |
json $.arr contains-all|contains-only|contains-any […] | Set-style array checks |
json $.arr contains-sequence […] | Ordered contiguous sublist |
json $.arr subset-of […] | Every element is in the given list |
json $.obj contains-keys […] / not contains-keys | Object keys present / absent |
json $.arr unique | No duplicate values |
json $.arr sorted / sorted desc | Ascending / descending order |
json $.n between LO HI | Inclusive numeric range |
json $.n close-to X delta D | Absolute tolerance (± also accepted) |
json $.n zero / positive / negative | Numeric sign checks |
json $.items each $.status == "active" | Assert each array element |
Headers
| Form | Meaning |
|---|---|
header Name == v / != | Exact value |
header Name contains / not contains | Substring |
header Name matches / not matches | Regex |
header Name starts-with / ends-with | Prefix / suffix |
header Name equals-ignoring-case / contains-ignoring-case | Case-insensitive |
header Name in […] / not in […] | Value in / not in list |
header Name empty / not empty | Empty string (missing treated as empty for empty/not empty) |
header Name absent / not exists | Header missing |
header Name exists / present | Header present |
Other checks
| Form | Meaning |
|---|---|
schema ./file.json / schema inline {…} | JSON Schema validation |
duration < 200ms | Request duration |
openapi ./spec.yaml [strict] | OpenAPI contract check |
xpath //Order/@id == "1" | XML path (stdlib; limited) |
The playground runs status, body, json, and header checks. Schema, XPath, duration, and OpenAPI print a playground error instead of failing silently.
SAVE, SET, and variables
SAVE: userId FROM $.id
SAVE: requestId FROM header X-Request-Id
SET: orderId ${uuid()}
${NAME} expands in URLs, paths, headers, bodies, and expect values. Lookup order: process environment, then --env / auto-discovered suite env / --profile, then SET / SAVE / CALL. Built-in functions: ${uuid()}, ${now()}, ${random.int(min,max)}. Custom functions use CALL. Undefined variables are errors.
The playground seeds ${TOKEN} as playground-token so bearer samples run without an env file. Plugins are CLI-only.
Plugins
A plugin is a Python function loaded from extensions/ (or snapapi.yaml / --plugin). The suite calls it with CALL: signature = crypto.generate_signature(${PAYLOAD}, ${SECRET}) and uses ${signature}. Arguments are explicit. The function returns a value; it does not mutate SnapAPI variables, assert, or issue the HTTP under test. CALL is computation, not a second HTTP engine. Exceptions fail that test as CALL FAILED, not a traceback. There is no IF / FOR / WHILE — see When SnapAPI is not for you and CALL.
WAIT and retries
WAIT: json $.status == "ready" TIMEOUT 10s BACKOFF 0.5s
EXPECT: json $.status == "ready" RETRY 20 BACKOFF 0.5s
WAIT reissues the current request until the check passes or the timeout expires. EXPECT … RETRY retries when the HTTP status is already retryable (for example 5xx with ON 5xx). The playground caps WAIT at 8 seconds so a tab cannot hang.
Modern vs legacy
Both parse. Prefer the left column in new files.
| Current | Also accepted |
|---|---|
GET: /users | REQUEST: GET /users |
BODY: {…} | DATA: {…} |
HEADER Content-Type: application/json | HEADERS: {"Content-Type": "application/json"} |
TAG: users write | TAG: users, write |
EXPECT: status == 201 | EXPECT: STATUS 201 |
EXPECT: body contains id | EXPECT: CONTAINS id |
EXPECT: json $.email == "a" | EXPECT: JSON $.email == "a" |