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
KeywordMeaning
SUITEDisplay name. Defaults to the file stem if omitted.
DESCSuite description, or test description when it sits under TEST.
URLBase URL. Can be set per test. Interpolates ${VAR}. https://api.example.com in samples is a placeholder — see Your API.
TIMEOUTHTTP timeout in seconds for the suite.
FOLLOW-REDIRECTSBoolean. Also allowed on a step.
OPTIONSJSON 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 / HEADERSDefault headers. Line form or JSON object.
AUTHDefault auth for following requests.
SETAssign a value without HTTP, e.g. SET: orderId ${uuid()}.
CALLCALL: signature = crypto.generate_signature(${PAYLOAD}, ${SECRET}). Stores the return value. See CALL.
IMPORTPull tests from another .sapi file (path relative to the current file). Not in the playground.
HELPERNamed 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-SETUPName 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-TEARDOWNRuns 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}
KeywordMeaning
TESTA case that runs and is counted. Names referenced by SETUP/TEARDOWN/DEPENDS must exist.
TAGSpace- or comma-separated tags. CLI --tag requires all given tags to match.
SETUP / TEARDOWNName a HELPER or TEST. Those procedures are not counted as cases. Cycles are parse errors.
DEPENDSName 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.
EXAMPLESInline CSV (header + rows) or a .csv path. Each row becomes Test [first-cell] with those variables. Playground supports inline tables only.
SKIP / ONLY / QUARANTINECLI filters. Not implemented in the playground (you get a clear error).
SETTest-scoped assignment, interpolated before steps.
CALLTest-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
KeywordMeaning
REQUEST: METHOD /pathLegacy / OPTIONS form. Method is one of GET POST PUT PATCH DELETE HEAD OPTIONS.
QUERYpage=2&limit=10 merged onto the path query string.
PARAMPARAM: page 2 or PARAM: page=2.
HEADER / HEADERSStep headers overlay test headers overlay suite headers.
AUTH: bearer TOKENSets Authorization: Bearer TOKEN. Also token, basic user:pass.
AUTH: digest user:passHTTP 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

FormMeaning
status == 200 / != 500HTTP status equals / not equals
status == 200 RETRY N …Retry when status is retryable (see WAIT)

Body

FormMeaning
body contains text / not containsSubstring present / absent
body matches regex / not matchesRegex search
body starts-with / ends-withPrefix / suffix
body empty / not emptyEmpty or non-empty response text

JSON

JSONPath subset: $.a.b, $.items[0].id, $.items[*].id, $.items[?(@.status=="open")].

FormMeaning
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 containsArray membership or string substring
json $.x matches / not matchesRegex on string value
json $.x starts-with / ends-withString prefix / suffix
json $.x equals-ignoring-caseCase-insensitive equality
json $.x contains-ignoring-caseCase-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 emptyEmpty container or string
json $.x absent / not existsZero matching paths (null still counts as present). With [*], every element must lack the field.
json $.x exists / presentAt least one matching path resolves. With [*], any element having the field is enough.
json $.x type string|number|array|object|boolean|nullJSON 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-keysObject keys present / absent
json $.arr uniqueNo duplicate values
json $.arr sorted / sorted descAscending / descending order
json $.n between LO HIInclusive numeric range
json $.n close-to X delta DAbsolute tolerance (± also accepted)
json $.n zero / positive / negativeNumeric sign checks
json $.items each $.status == "active"Assert each array element

Headers

FormMeaning
header Name == v / !=Exact value
header Name contains / not containsSubstring
header Name matches / not matchesRegex
header Name starts-with / ends-withPrefix / suffix
header Name equals-ignoring-case / contains-ignoring-caseCase-insensitive
header Name in […] / not in […]Value in / not in list
header Name empty / not emptyEmpty string (missing treated as empty for empty/not empty)
header Name absent / not existsHeader missing
header Name exists / presentHeader present

Other checks

FormMeaning
schema ./file.json / schema inline {…}JSON Schema validation
duration < 200msRequest 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.

CurrentAlso accepted
GET: /usersREQUEST: GET /users
BODY: {…}DATA: {…}
HEADER Content-Type: application/jsonHEADERS: {"Content-Type": "application/json"}
TAG: users writeTAG: users, write
EXPECT: status == 201EXPECT: STATUS 201
EXPECT: body contains idEXPECT: CONTAINS id
EXPECT: json $.email == "a"EXPECT: JSON $.email == "a"