Skip to content
Read API

Conventions

Every endpoint shares one key, one error shape and one rate limit.

Every request goes to https://app.squally.dev/api/v1 plus the path and carries an organization read key as Authorization: Bearer sqly_ro_…. With the key in SQUALLY_API_KEY - the variable the MCP server reads too - the first request lists the organization’s projects:

curl -H "Authorization: Bearer $SQUALLY_API_KEY" https://app.squally.dev/api/v1/projects

A read key is organization-wide (every project), created by an organization admin in Organization › Read keys, and expires after 30, 60, 90, 180 days as chosen at creation. There is no renewal: create a new key and revoke the old one. Project ingest keys (sqly_…) are refused with wrong_key_type.

Every error is {"error": "<sentence>", "code": "<slug>"}. The sentence may be reworded; the slug may not. An id that does not exist and an id that belongs to another organization get the same 404, byte for byte.

HTTPCodeWhenWhat to do
400ambiguous_testSeveral tests have this name. Pass ?filePath= with one of: tests/a.spec.ts, tests/b.spec.ts.Repeat the request with ?filePath= set to one of the files the sentence lists.
400invalid_parametersha must be 4 to 40 hexadecimal characters (a commit SHA or a prefix of one).Fix the request; do not retry.
400invalid_perioddays=90 is beyond this plan’s 60-day retention. Use one of 7, 14, 30.Use days=7, 14, 30 or 90, within the plan’s retention; do not retry.
401invalid_keyInvalid read key.Check the key for typos or create a new one in Organization › Read keys; do not retry.
401key_expiredThis read key expired on 2026-09-01. Create a new one in Organization › Read keys.Create a new read key in Organization › Read keys; keys are not renewed.
401key_revokedThis read key has been revoked.Create a new read key in Organization › Read keys; this one will not work again.
401missing_keyMissing read key.Send the read key as Authorization: Bearer sqly_ro_…; do not retry without it.
401wrong_key_typeThis is a project ingest key. Read routes need an organization read key - Organization › Read keys.Use an organization read key (sqly_ro_…), not a project ingest key; do not retry.
403organization_deletedThis organization has been deleted. It can be restored until 2026-11-22: contact support@squally.dev.Ask Squally support to restore the organization before the date the sentence names; do not retry.
403tier_too_lowThe read API requires the Standard plan. This organization is on Free.Upgrade the organization to Standard or Pro; the same key works again after the upgrade.
404error_not_foundNo error signature with this fingerprint in this project.Take the fingerprint from GET /projects/{projectId}/errors; do not retry.
404project_not_foundNo project with this id in this organization.Take the project id from GET /projects; do not retry.
404run_not_foundNo run with this id in this project.Take the run id from GET /projects/{projectId}/runs; do not retry.
404test_not_foundNo test with this name in this project.Check the test name and filePath exactly as Squally shows them; do not retry.
429rate_limitedMore than 60 requests within 60 seconds with one read key.Wait the seconds Retry-After names (60), then retry. Requests are counted per key in 60-second windows.
500internal_errorSomething went wrong on our side.Retry with exponential backoff, starting at 1 second and capped at 30 seconds.
Retry only on 429 and 5xx

429: wait the seconds Retry-After names (60), then retry. Requests are counted per key in 60-second windows. 5xx: retry with exponential backoff, starting at 1 second and capped at 30 seconds. 400, 401, 403, 404: never retry - the same request gets the same answer.

Currently 60 requests per 60 seconds per key - a read key has its own budget, shared with nothing else. Over the limit the API answers 429 with Retry-After: 60 and the usual error body, {"error": "Too many requests with this read key. Wait 60 seconds, then retry.", "code": "rate_limited"}. A caller far beyond the limit can be stopped earlier by the edge firewall with a bare 429 - no body, no Retry-After: wait 60 seconds just the same.

/projects and /projects/{projectId}/branches answer with every item at once, no paging. /runs pages with a cursor (cursor, direction, perPage); /tests and /errors page by number (page, 1-based - a page past the end answers the last page - and perPage).

listTests and listErrors page by number: Squally computes the whole list and returns the page you ask for. listRuns pages with a cursor.

  • page: 1-based page number. A page past the end answers the last page.
  • perPage: Rows per page. listRuns and listErrors take 10, 25 or 50 (default 10), listTests 1 to 100 (default 50).
  • cursor: Opaque. Pass nextCursor for older runs, or prevCursor together with direction=prev for newer runs.
  • direction: Which way the cursor pages.

A run’s total, passed, failed, flaky and skipped count tests, each test once over all the run’s shards by its final attempt: passed on the first try, flaky after a retry, failed when the final attempt failed; total = passed + failed + skipped + flaky. Figures over a period - a test’s runs, an error’s occurrences - count finished runs of the source parameter’s kind, as each field’s description says.

runNumber is the run’s number in its project, the #12 the app shows: counted up from 1 per project, one number for all shards of a run, never given twice. Runs removed by the plan’s retention leave gaps. The run routes take the run’s id, not its number.

days: The period, in days, ending now. Must not exceed the plan’s retention (Standard 60, Pro 90 days); a longer period is refused with 400 invalid_period rather than answered with less data.

The test metrics and the error figures count runs whose shards all finished, a sharded run once. Six endpoints - getOverview, listTests, getTestMetrics, getTestHistory, listErrors and getError - take a source parameter that picks the runs by where they ran (since 1.0.0-beta.8; on the two error endpoints since 1.0.0-beta.9):

source: Which runs count, by where they ran: all - every run (the default since 1.0.0-beta.18; ci before); ci - runs a shard reported a CI provider for; local - runs no shard did. The response’s population names the one counted.

Without source, every endpoint counts all runs, CI and local (since 1.0.0-beta.18; CI runs only before). Send source=ci for CI runs only - the figures the endpoints gave before beta.18. Any value other than ci, local or all answers 400 invalid_parameter.

listRuns takes source too (since 1.0.0-beta.14), but it lists runs and counts nothing, so it lists every run when source is absent - the dashboard’s runs list does the same. Each run says whether it was local:

source on listRuns: Which runs are listed, by where they ran: all - every run (the default); ci - runs a shard reported a CI provider for; local - runs no shard did (each run’s local field). Since 1.0.0-beta.14.

The response’s population names the runs it counted. The test metrics - listTests, getTestMetrics and the overview’s testMetrics - count each test per Playwright project, as the Test explorer does; the app’s Overview counts a test once across projects, so its Failing tests and Flaky tests can be lower. They leave out runs that were a mass failure as a whole:

Which runs every number counts: runs whose shards all finished, each counted once however many shards it ran in, placed in the period by the start of their first shard - without runs that were a mass failure as a whole (at least 20 tests, over 80% of them failed). Every branch unless the branch parameter names one. A run finishes when all its shards have, or - once it is overdue by the project’s run timeout, counted from its latest shard’s start - when every shard that started has completed, timed out or been stopped by the monthly result limit; such a run counts like a cancelled one, with the results its shards reported (since 1.0.0-beta.10; the monthly result limit since 1.0.0-beta.11). Runs still running are not counted. The prefix names the source parameter: ci_ - CI runs (a shard reported a CI provider); local_ - runs no shard reported a CI provider for; all_ - both, the default since 1.0.0-beta.18 (ci_ before it; since 1.0.0-beta.8; before it, ci_groups_finished_not_mass_failure was the only value, and since 1.0.0-beta.6 - it was ci_completed_not_excluded, which counted every completed shard on its own).

The error figures of listErrors and getError count those runs, and say how many of an error’s runs were one (massFailureRunCount):

Which runs every error figure counts: runs whose shards all finished, each counted once however many shards it ran in, placed in the period by the start of their first shard - runs that were a mass failure as a whole included (see massFailureRunCount). A run finishes when all its shards have, or - once it is overdue by the project’s run timeout, counted from its latest shard’s start - when every shard that started has completed, timed out or been stopped by the monthly result limit; such a run counts like a cancelled one, with the results its shards reported (since 1.0.0-beta.10; the monthly result limit since 1.0.0-beta.11). Runs still running are not counted. The prefix names the source parameter: ci_ - CI runs (a shard reported a CI provider); local_ - runs no shard reported a CI provider for; all_ - both, the default since 1.0.0-beta.18 (ci_ before it; since 1.0.0-beta.9).

Which runs that is, in plain words: How Squally measures tests and Local runs.