Skip to content

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.

https://api.marriska.com/api/v1

Every endpoint is prefixed with /api/v1. There’s no unversioned path — versioning lives in the URL.

The API supports two auth mechanisms; pick whichever fits the call site.

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.

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.

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.

  • Content type: application/json for 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 answers 422 with detail as a list, one entry per problem: loc (where, e.g. ["body", "session_cookies", 0, "value"]), msg and type. 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 sets Cache-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).

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.

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.

TagPrefixWhat it covers
Authentication/authEmail/password login, signup, password reset
OAuth/authGoogle / GitHub OAuth start + callback
Sessions/auth/sessionsList and revoke active sessions
Email Verification/authEmail address verification flow
Password Reset/authAsk for a reset link by email, and set a new password with it (no auth)
Tests/testsCreate, read, update, delete tests; duplicate tests
Test Steps/tests/{id}/stepsEdit, reorder, and delete the steps inside a test
Test Sets/test-setsNamed collections of tests
Test Set Items/test-setsAdd/remove tests inside a set, reorder
Tags—Apply and remove tags from tests
Folders/foldersFolder tree for organizing tests
Projects/projectsTop-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/testsTranslate plain English into structured steps
Export/testsExport a test (YAML, .marriska, or Playwright Python)
Test Export—Test-set-level export
Executions/executionsRead past run details and per-browser results
Reports/reportsShareable run reports + share links
Runner/runnerCloud run dispatch and control
Runs/runsRun lifecycle endpoints
Screenshots/testsRead step screenshots from a run
Baselines/testsManage visual regression baselines (per step + browser)
Schedules/schedulesCron-style scheduled runs
Notifications/notificationsNotification contacts and preferences
Organizations/organizationsOrg settings and metadata
Members—List members; change roles; remove
Invites/invitesInvite team members; accept invitations
API Keys/api-keysCreate, list, revoke API keys
CLI/cliEndpoints called by marriska-runner (require API key auth)
Plan/planRead current plan; cancel scheduled changes
PlanChange/plan/changeCreate, confirm, schedule, apply plan changes
Catalog/catalogPublic plan catalog (no auth)
Entitlements/entitlementsComputed capabilities for the current user/org
Settings/settingsPer-user settings (preferences; BYOK keys are operator-gated and off by default)
Usage/usagePer-org usage counters and quota readouts
Dashboard/dashboardAggregates for the dashboard page
Health—Liveness checks

Two real-time channels run alongside the REST API. Both speak JSON messages.

EndpointPurpose
/api/v1/ws/executionBrowser-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/agentPersistent 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.

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.

MethodPathPurpose
GET/api/v1/cli/whoamiThe organization and key name a call carries — print it before writing anywhere
GET/api/v1/cli/projectsList projects
PUT/api/v1/cli/projects/{id}Rename a project or set the hosts its tests may navigate to
GET/api/v1/cli/testsList tests with step counts
GET/api/v1/cli/tests/{id}/specsOne test with its ordered steps
POST/api/v1/cli/testsCreate 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}/stepsReplace a test’s steps
POST/api/v1/cli/cloud-runsQueue 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.

MethodPathNotes
GET/api/v1/cli/environments/{id}/variablesNames 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}/variablesrequired (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.

GET /api/v1/cli/cloud-runs/{id} returns three fields for this, and the one to read is verdict.

verdictMeaningTreat as
passedEvery test asked for ran and passedgreen
passed_with_skipsEverything that ran passed; the rest were skips the author declaredamber — green, with the skip count shown
failedSomething failed, or something was asked for and never answeredred
nothing_ranNothing was tested: an empty set, or every test skipped or unrunred
stoppedA person stopped itneither
nullNot decided yet. The run is not finalisednot 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 — alongside test_set_ids;
  • each item has test_name alongside test_id.

So a summary can say “Regression → Checkout total failed” instead of “4f21… → 066946e4… failed”. The ids stay, for re-running it.

origin is null whenever the verdict is green or amber — a pass attributes no fault, and neither does a declared skip. Otherwise:

originMeaning
configurationYour 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_applicationThe site under test did something the test did not expect. The only origin that means “this is a real finding”.
oursOur 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.

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_reasonWhat happenedVerdict / origin
reportedThe run reached its end and reported it. The verdict and the tests say the rest; there is no messageany
stopped_by_personA person stopped itstopped / —
removed_by_personA person removed it from the queue before it startedstopped / —
agent_leftYour local agent stopped answering and did not reconnect within 90 seconds; the tests it had not reported failed by namefailed / configuration
no_agentA local run no agent picked up within 24 hours of being queuedfailed / configuration
refusedRefused before it started, for its own setup; end_reason_params.refusal says which — foreign_resource, nothing_to_dispatch, zero_step_test or tests_not_ownedfailed / configuration
server_restartedOur server restarted while it was in progress and it could not be resumedfailed / ours
server_errorAn internal error on our side ended itfailed / ours
queue_expiredIt waited in our queue past its limit (2 hours) without startingfailed / ours
stale_skippedA scheduled run skipped because the schedule’s stale window passed before our queue reached itfailed / ours
unknownIt ended without a decision we could verify, so we treat it as oursfailed / 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:

statusPassed?Notes
completedyesA passing test says completed, not passed
failedno
setup_failedno — counts as a failureA precondition broke, so the test never ran. Filtering items for status == "failed" can therefore find nothing on a red run
skippednoskip_reason says why: user_skipped is declared, no_steps and already_skipped are not
stoppedno
queued, runningnot yet

The item vocabulary is the reason the run-level verdict exists. Reading it spares you this table.

/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.

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):

Terminal window
curl https://api.marriska.com/openapi.json

Pipe 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.

  • CLI reference — the marriska-runner and marriska-agent binaries that call this API
  • API keys — creating, scoping, and rotating the keys that authenticate Bearer calls
  • Security model — auth, isolation, and what gets logged