Creating API Scenarios
An API scenario is a sequence of HTTP requests with assertions, variable extraction, and authentication. Think of it as a test case for your API.
Basic Scenario
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
$CSS_URL/api/scenarios \
-d '{
"name": "User CRUD Lifecycle",
"projectId": 1,
"environmentId": 1,
"baseUrl": "{{baseUrl}}",
"steps": [
{
"stepOrder": 1,
"name": "Create user",
"httpMethod": "POST",
"endpointPath": "/api/users",
"requestBody": "{\"name\":\"John\",\"email\":\"john@example.com\"}",
"expectedStatusCodes": [201],
"extractions": [
{
"variableName": "userId",
"extractionType": "JSONPATH",
"extractionExpression": "$.id",
"isRequired": true
}
]
},
{
"stepOrder": 2,
"name": "Read user",
"httpMethod": "GET",
"endpointPath": "/api/users/{{userId}}",
"expectedStatusCodes": [200],
"responseAssertions": [
{
"type": "JSONPATH",
"expression": "$.name",
"operator": "EQUALS",
"expectedValue": "John"
}
]
},
{
"stepOrder": 3,
"name": "Delete user",
"httpMethod": "DELETE",
"endpointPath": "/api/users/{{userId}}",
"expectedStatusCodes": [200, 204]
}
]
}'
Key Concepts
Choosing the base URL (host)
baseUrl is optional. The recommended model: author the scenario against an application
(bind a targetId, or set appTag / applicationId) and pass no host at all — the host
(targetHost) resolves at run time from the environment's target for that application. Create
the application and environment once, set the target's URL inside the environment, and every test
authored against that app runs against whichever environment you pick, hitting that environment's
URL. One scenario, every environment, zero hardcoded hosts.
Ways to supply the host, from most to least recommended:
| Form | Resolves from | Use when |
|---|---|---|
omit baseUrl + a bound target / appTag | the matching target in the run environment, at run time | the default — portable tests, host lives on the environment |
"{{baseUrl}}" + an appTag (or a bound target) | same as above (explicit placeholder form) | equivalent to omitting; kept for existing scenarios |
"{{myhost}}" (any name) + an env property | the environment's variable bag | a quick test that shouldn't need a full target row |
"https://api.example.com" | the literal value | a fixed, single-environment host (an explicit override) |
A scenario with no baseUrl and no target/appTag is refused at create time — it could never
resolve a host.
The self-documenting alias "{{env.myhost}}" resolves to the same property as "{{myhost}}" —
use whichever reads more clearly.
The third form (env property) is the lightest setup. Store the host once on the environment:
# set a property named `myhost` on the environment
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
$CSS_URL/api/environments/1/variables \
-d '[{"key":"myhost","value":"https://api.example.com"}]'
…then author the scenario with "baseUrl": "{{myhost}}" and no target. The host resolves
from the property at run time, per environment (each env can define its own myhost). If a
templated host resolves to neither a target nor a matching property, the scenario is refused at
create time rather than failing on every run.
Property key rules: a key must match ^[a-zA-Z][a-zA-Z0-9_]*$ (start with a letter,
alphanumeric/underscore, no dots or spaces), be ≤64 characters, and is case-insensitive —
myhost and MyHost are the same key, so setting MyHost updates the existing myhost rather
than creating an invisible duplicate. update_env_variable only edits an existing key (it does not
create — a missing key returns 404); use set_env_variables to create.
When a property name collides with a higher-precedence source — a resolved target's URL, or a
credential field like username — the more specific source wins and the property is ignored
(with a warning in the run log). Name host properties distinctly (myhost, not baseUrl) if you
want them to apply unconditionally. Property-driven hosts apply to every surface — API
scenarios, UI tests, performance/Artillery runs, and security scans (a scan target.value of
"{{myhost}}" resolves identically).
A property marked isSecret is not usable as a host or scan target — it can't be reliably
masked once it lands in a URL or an Artillery config. Use secret properties only for {{var}}
values inside API/UI tests, and keep real credentials in the environment credential vault
(add_environment_credential), never in a property.
Variable Extraction
Extract values from responses and use them in later steps:
"extractions": [
{
"variableName": "userId",
"extractionType": "JSONPATH",
"extractionExpression": "$.id"
}
]
Reference with {{userId}} in subsequent steps — URLs, headers, bodies.
Assertions
Validate responses:
"responseAssertions": [
{"type": "JSONPATH", "expression": "$.status", "operator": "EQUALS", "expectedValue": "active"},
{"type": "JSONPATH", "expression": "$.items", "operator": "EXISTS", "expectedValue": ""},
{"type": "JSONPATH", "expression": "$.count", "operator": "GREATER_THAN", "expectedValue": "0"}
]
Operators: EQUALS, NOT_EQUALS, CONTAINS, EXISTS, NOT_EXISTS, GREATER_THAN, LESS_THAN
Authentication
Proofarc supports 5 authentication types:
| Type | Use Case |
|---|---|
| Bearer (login endpoint) | Most REST APIs — auto-fetches token via login |
| OAuth2 Client Credentials | Enterprise APIs, Auth0, Okta |
| Static Bearer Token | CI/CD pipelines, service accounts |
| Basic Auth | Legacy APIs |
| API Key | Public APIs with rate limiting |
Example — Bearer with login endpoint:
"authConfig": {
"authType": "BEARER",
"tokenSource": "LOGIN_ENDPOINT",
"loginEndpoint": "/auth/login",
"tokenJsonPath": "$.accessToken",
"credentials": {
"username": "admin",
"password": "your-password"
}
}
YAML Format
Prefer writing scenarios in YAML? The same scenario can be written as:
name: User CRUD Lifecycle
baseUrl: "{{baseUrl}}"
steps:
- name: Create user
method: POST
path: /api/users
body:
name: John
email: john@example.com
expect: [201]
extract:
userId: $.id
- name: Read user
method: GET
path: /api/users/{{userId}}
expect: [200]
assert:
- path: $.name
equals: John
- name: Delete user
method: DELETE
path: /api/users/{{userId}}
expect: [200, 204]
Create via API: POST /api/scenarios/yaml?projectId=1&environmentId=1
See YAML Format for full reference and editor usage.
Editing Steps in the UI
Open an existing scenario and click Edit to manage its steps directly:
- Add Step — the button at the top of the Steps section appends a new step (defaults to
GET /); expand it to fill in the method, path, expected status codes, headers, body, and extractions, then click Save Step. - Edit — expand any step to change its fields inline and Save Step.
- Reorder — use the up/down arrows on each step.
- Delete — remove a step with the trash icon.
You can also import steps from a Swagger/OpenAPI spec via the Scenario Wizard.
What Happens Next
Your API scenario is a test case. From here it can:
- Execute → Run against any environment, get pass/fail per step
- Convert to Performance Test → Same endpoints, same auth, now with concurrent users
- Feed the Readiness Score → Pass rate contributes to your 0-100 score
- Map to Compliance → Test execution evidence auto-maps to SOC 2, PCI-DSS controls