REST API reference
The Marriska HTTP API is a FastAPI service. Schemas, validation rules, and per-endpoint detail are auto-generated from the live OpenAPI spec — this page covers the parts that aren’t worth re-generating: base URL, auth, conventions, and the high-level resource map.
Base URL
Section titled “Base URL”https://api.marriska.com/api/v1Every endpoint is prefixed with /api/v1. There’s no unversioned path —
versioning lives in the URL.
Authentication
Section titled “Authentication”The API supports two auth mechanisms; pick whichever fits the call site.
API key (CLI, CI, server-to-server)
Section titled “API key (CLI, CI, server-to-server)”Send the key as a Bearer token. Keys all start with ak_live_:
Authorization: Bearer ak_live_•••Create keys under Settings → API Keys in the web UI. See Creating an API key. Keys are scoped to one organization and can be revoked at any time.
API-key auth is required for the /api/v1/cli/* endpoints used by
marriska-runner. Mismatched, missing, or revoked keys return 401.
Session cookie (browser, web UI)
Section titled “Session cookie (browser, web UI)”The web app authenticates via a server-issued httpOnly refresh cookie
(refresh_token) plus a short-lived access token held in memory. The
cookie is created by the email/password login or OAuth flows under
/api/v1/auth/* and refreshed silently as the access token expires.
You almost certainly don’t want to drive the API this way unless you’re
the web frontend.
Org context
Section titled “Org context”Many endpoints operate on the caller’s currently-active organization,
resolved from the API key (one key, one org) or the session’s X-Org-Id
header. Cross-org reads return 403.
Endpoints that name an organization in their path
(/organizations/{org_id}/…) act on that organization instead, and check
the caller’s membership and role in it — not in the X-Org-Id
organization. A caller who is not an active member of the named
organization gets 403 Not a member of this organization.
Conventions
Section titled “Conventions”- Content type:
application/jsonfor both requests and responses. - Timestamps: ISO 8601 in UTC (e.g.
2026-04-26T12:34:56.789Z). - IDs: UUIDs as strings.
- Errors:
{ "detail": "..." }body with the appropriate HTTP status. A request the API cannot parse answers422withdetailas a list, one entry per problem:loc(where, e.g.["body", "session_cookies", 0, "value"]),msgandtype. The value you sent is never repeated back — a refused cookie, password or key does not end up in your CI log through the error. - Pagination: not all list endpoints paginate. When they do, parameters are documented in the OpenAPI spec.
- Cache: every
/api/*response setsCache-Control: no-store, no-cache, must-revalidate— don’t cache.
The API allows browser requests from origins listed in
CORS_ORIGINS. Allowed methods: GET, POST, PUT, DELETE, OPTIONS.
Allowed headers include Authorization, Content-Type, X-Org-Id, and
X-Requested-With. Credentials are allowed (cookie auth).
Security headers
Section titled “Security headers”Every response carries X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, X-XSS-Protection: 1; mode=block, and
Referrer-Policy: strict-origin-when-cross-origin.
Resource map
Section titled “Resource map”The API is grouped by tag. The production OpenAPI spec describes, in full,
every endpoint an API key can call — the CLI tag below — and the
unauthenticated ones; the other tags are the web app’s own API, mapped here
at the level of resources.
| Tag | Prefix | What it covers |
|---|---|---|
Authentication | /auth | Email/password login, signup, password reset |
OAuth | /auth | Google / GitHub OAuth start + callback |
Sessions | /auth/sessions | List and revoke active sessions |
Email Verification | /auth | Email address verification flow |
Password Reset | /auth | Ask for a reset link by email, and set a new password with it (no auth) |
Tests | /tests | Create, read, update, delete tests; duplicate tests |
Test Steps | /tests/{id}/steps | Edit, reorder, and delete the steps inside a test |
Test Sets | /test-sets | Named collections of tests |
Test Set Items | /test-sets | Add/remove tests inside a set, reorder |
Tags | — | Apply and remove tags from tests |
Folders | /folders | Folder tree for organizing tests |
Projects | /projects | Top-level project containers. Each response names its default_environment. PUT /projects/{id} takes a name and a description only; an allowlist or the agent flag there is a 422 naming the field |
Environments | /projects/{id}/environments, /environments/{id} | Where a project’s tests are aimed — base URL, allowed_navigate_hosts, allowed_api_hosts, and (owner/admin) session_for_agent_tests. /environments/{id}/session stores a signed-in session for that environment: write-only, owner/admin |
Parse | /tests | Translate plain English into structured steps |
Export | /tests | Export a test (YAML, .marriska, or Playwright Python) |
Test Export | — | Test-set-level export |
Executions | /executions | Read past run details and per-browser results |
Reports | /reports | Shareable run reports + share links |
Runner | /runner | Cloud run dispatch and control |
Runs | /runs | Run lifecycle endpoints |
Screenshots | /tests | Read step screenshots from a run |
Baselines | /tests | Manage visual regression baselines (per step + browser) |
Schedules | /schedules | Cron-style scheduled runs |
Notifications | /notifications | Notification contacts and preferences |
Organizations | /organizations | Org settings and metadata |
Members | — | List members; change roles; remove |
Invites | /invites | Invite team members; accept invitations |
API Keys | /api-keys | Create, list, revoke API keys |
CLI | /cli | Endpoints called by marriska-runner (require API key auth) |
Plan | /plan | Read current plan; cancel scheduled changes |
PlanChange | /plan/change | Create, confirm, schedule, apply plan changes |
Catalog | /catalog | Public plan catalog (no auth) |
Entitlements | /entitlements | Computed capabilities for the current user/org |
Settings | /settings | Per-user settings (preferences; BYOK keys are operator-gated and off by default) |
Usage | /usage | Per-org usage counters and quota readouts |
Dashboard | /dashboard | Aggregates for the dashboard page |
Health | — | Liveness checks |
WebSockets
Section titled “WebSockets”Two real-time channels run alongside the REST API. Both speak JSON messages.
| Endpoint | Purpose |
|---|---|
/api/v1/ws/execution | Browser-driven test execution: step events stream from cloud runs back to the web UI. Sign in with ?ticket= — a single-use, 30-second ticket from POST /auth/ws-ticket; an access token in the URL is refused (close code 4001, with the reason) |
/api/v1/ws/agent | Persistent connection used by marriska-agent to receive dispatches and report results. The API key goes in the Authorization: Bearer header; a key in the URL is refused (close code 4001, with the reason) |
Auth: the agent endpoint accepts the same ak_live_ API key (sent
during the connect handshake). Browser WS connections piggy-back on the
session cookie.
Automation endpoints (API key)
Section titled “Automation endpoints (API key)”These accept an ak_live_… API key (every plan includes one) as
Authorization: Bearer <key> — no session, no WebSocket. They are what the
CLI runner, the MCP server, and CI
scripts use.
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/cli/whoami | The organization and key name a call carries — print it before writing anywhere |
GET | /api/v1/cli/projects | List projects |
PUT | /api/v1/cli/projects/{id} | Rename a project or set the hosts its tests may navigate to |
GET | /api/v1/cli/tests | List tests with step counts |
GET | /api/v1/cli/tests/{id}/specs | One test with its ordered steps |
POST | /api/v1/cli/tests | Create a test from explicit steps — in a project_id, or a folder_id (which names its project); with neither, 422 |
PUT | /api/v1/cli/tests/{id}/steps | Replace a test’s steps |
POST | /api/v1/cli/cloud-runs | Queue a cloud run → run_ids |
GET | /api/v1/cli/cloud-runs/{id} | Run status + per-test results |
Steps use the public field names from
Step actions (action, target, value,
description). Cloud runs are metered against the same monthly quota as the
UI and scheduler. A start that needs more runs than are left this month is
refused with 429, saying how many it needs and how many are left: one run
per test set, or one for a list of tests. A 503 means we could not check the
month in time; nothing was started, and you can retry. Runs started this way show
triggered_by: api:<key name> in the run history.
Environment variables
Section titled “Environment variables”| Method | Path | Notes |
|---|---|---|
GET | /api/v1/cli/environments/{id}/variables | Names and non-secret values. A secret’s value is never returned. |
PUT | /api/v1/cli/environments/{id}/variables/{name} | {"value": "…", "is_secret": false} |
DELETE | /api/v1/cli/environments/{id}/variables/{name} | 404 when there is nothing to delete |
GET | /api/v1/cli/projects/{id}/variables | required (derived from the tests), per-environment defined/missing, and the descriptors |
PUT | /api/v1/cli/projects/{id}/variables/{name}/descriptor | {"description": "…", "default_value": "…"} — refused for a name that is secret anywhere |
A run whose steps reference a name nothing supplies is refused, naming the variable, the environment and the step — see the guide for the two names that survive on purpose, and why a previously-green test that now refuses was not testing what it claimed.
Deciding whether a run passed
Section titled “Deciding whether a run passed”GET /api/v1/cli/cloud-runs/{id} returns three fields for this, and the one to
read is verdict.
verdict | Meaning | Treat as |
|---|---|---|
passed | Every test asked for ran and passed | green |
passed_with_skips | Everything that ran passed; the rest were skips the author declared | amber — green, with the skip count shown |
failed | Something failed, or something was asked for and never answered | red |
nothing_ran | Nothing was tested: an empty set, or every test skipped or unrun | red |
stopped | A person stopped it | neither |
null | Not decided yet. The run is not finalised | not a pass |
A null verdict is never a pass. Reading it as one turns a timed-out poll into a success, which is the whole reason the field is nullable rather than defaulted.
Do not derive the result from failed_tests == 0. A run of ten tests where
none ran has zero failures, and so does an empty set. The verdict is computed
against the tests enrolled, which is the only way to see a test that never
answered.
Naming what failed, without a second request
Section titled “Naming what failed, without a second request”Both the run detail and the run history carry names, not only ids, because the one line a human reads in a failing CI job has to be actionable on its own:
- the run has
set_names— the test sets it ran — alongsidetest_set_ids; - each item has
test_namealongsidetest_id.
So a summary can say “Regression → Checkout total failed” instead of “4f21… → 066946e4… failed”. The ids stay, for re-running it.
Whose problem it is
Section titled “Whose problem it is”origin is null whenever the verdict is green or amber — a pass attributes no
fault, and neither does a declared skip. Otherwise:
origin | Meaning |
|---|---|
configuration | Your setup: an environment that does not resolve, a session the allowlist does not cover, a test with no steps, an empty set, an action the cloud cannot run, a step that names no element, a check with nothing to compare. Ours to explain, yours to fix. |
tested_application | The site under test did something the test did not expect. The only origin that means “this is a real finding”. |
ours | Our defect. Do not go looking at your own setup; tell us. |
A failed step’s own diagnosis decides between the last two, and its message
says the same thing: an element the page shows wrong or not at all
(site_bug, not_ready) is tested_application; an element we could not
reach (tooling_gap — inside an iframe, say), several equally good matches
(ambiguous), or a page we could not inspect (unknown) is ours.
At run level the most attributable one wins: ours over configuration over
tested_application. If any part of a failure was ours, you should hear that
first rather than start by debugging your own application.
Why it ended
Section titled “Why it ended”Every finished run carries end_reason — a closed code — and
end_reason_message, the sentence we render from it. The report and the
run-report email show the same sentence, and for a run that never started it
is the only reason anywhere, because such a run has no tests to explain it.
Branch on the code; show the message.
end_reason | What happened | Verdict / origin |
|---|---|---|
reported | The run reached its end and reported it. The verdict and the tests say the rest; there is no message | any |
stopped_by_person | A person stopped it | stopped / — |
removed_by_person | A person removed it from the queue before it started | stopped / — |
agent_left | Your local agent stopped answering and did not reconnect within 90 seconds; the tests it had not reported failed by name | failed / configuration |
no_agent | A local run no agent picked up within 24 hours of being queued | failed / configuration |
refused | Refused before it started, for its own setup; end_reason_params.refusal says which — foreign_resource, nothing_to_dispatch, zero_step_test or tests_not_owned | failed / configuration |
server_restarted | Our server restarted while it was in progress and it could not be resumed | failed / ours |
server_error | An internal error on our side ended it | failed / ours |
queue_expired | It waited in our queue past its limit (2 hours) without starting | failed / ours |
stale_skipped | A scheduled run skipped because the schedule’s stale window passed before our queue reached it | failed / ours |
unknown | It ended without a decision we could verify, so we treat it as ours | failed / ours |
A local run that lost its agent keeps the status disconnected; every other
ending above reads failed, stopped or removed. end_reason is null for
a run still in flight and for runs finished before the field existed.
Item statuses, and which count as failures
Section titled “Item statuses, and which count as failures”An item’s status is the detailed state machine and its verdict is the same
five-value judgement as the run’s. If you read status, read this table:
status | Passed? | Notes |
|---|---|---|
completed | yes | A passing test says completed, not passed |
failed | no | |
setup_failed | no — counts as a failure | A precondition broke, so the test never ran. Filtering items for status == "failed" can therefore find nothing on a red run |
skipped | no | skip_reason says why: user_skipped is declared, no_steps and already_skipped are not |
stopped | no | |
queued, running | not yet |
The item vocabulary is the reason the run-level verdict exists. Reading it
spares you this table.
Webhooks
Section titled “Webhooks”/api/v1/webhooks/paddle receives Paddle events (Paddle is the production
payment provider and Merchant of Record). The handler verifies the
HMAC-SHA256 signature over ts:body (5-minute freshness window), dedupes by
event id through the webhook_events table, and applies:
transaction.completed (plan purchase), subscription.updated (tier
change), subscription.canceled (drop to Free at period end), and
adjustment.created with action refund/chargeback (immediate revoke).
/api/v1/webhooks/stripe receives Stripe events (alternative provider). The handler verifies
the signature via stripe.Webhook.construct_event, dedupes by event id
through a webhook_events table, and applies plan changes for
checkout.session.completed events (with payment_status=paid and a
plan_change_request_id in metadata). Bad signatures return 400,
events we don’t act on return 200 "ignored". When PAYMENT_PROVIDER=dev
the route also returns 200 "ignored" so misconfigured proxies don’t
fail loudly in test environments.
The webhook is inert by default (PAYMENT_PROVIDER=dev) — it only
verifies and applies events under PAYMENT_PROVIDER=stripe with
STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET set.
Outbound webhooks (Marriska → your URL) aren’t implemented yet. When they ship, they’ll land here as a separate page.
Fetching the spec
Section titled “Fetching the spec”The production API serves the OpenAPI spec of its public surface — every
endpoint an API key can call (/api/v1/cli/…) and the unauthenticated ones
(sign-in, the plan catalog, health):
curl https://api.marriska.com/openapi.jsonPipe it into your code generator of choice (openapi-typescript,
openapi-generator, etc.) for typed clients. Schemas are stable within
/api/v1; breaking changes would land under /api/v2.
See also
Section titled “See also”- CLI reference — the
marriska-runnerandmarriska-agentbinaries that call this API - API keys — creating, scoping, and rotating the keys that authenticate
Bearercalls - Security model — auth, isolation, and what gets logged