API keys

TestOrchestrator has two kinds of API key. A personal key connects an MCP client and acts as you. A workspace service key authenticates a CI pipeline on the public API, https://api.testorchestrator.com/api/v1, to upload automated test results. This page covers service keys: who creates them, their scopes and project, plan limits, rotation, a CI quick start and every error code.

Result upload opens in a following release; keys can be created now. Workspace service keys, their scopes and project binding are live. Until result upload opens, POST /api/v1/ingest/results checks the key and its scope and then answers 503 INGEST_NOT_AVAILABLE with Retry-After: 3600. When it opens, an accepted upload is recorded and answered 202; turning its results into a test run arrives after that.

Two kinds of key

Both live on Settings → API keys (under Personal), on two tabs. They are not interchangeable: each one works on exactly one surface and is refused everywhere else.

PropertyPersonal keyWorkspace service key
TabPersonal keysWorkspace service keys
ForAn MCP client (Claude, Cursor, Claude Code) acting as youA CI pipeline uploading automated results
Prefixto_live_to_svc_
Who creates itAny member, for themselvesA Tenant Admin; every member can see the list
Scopesmcp onlyingest:results, plus read if you tick it
ProjectFollows your own project accessOne project, or explicitly all projects
Works onhttps://api.testorchestrator.com/mcphttps://api.testorchestrator.com/api/v1
Refused on the other403 SCOPE_REQUIRED on /api/v1401 on /mcp, like an unknown key

Personal keys are covered in MCP Server. The rest of this page is about workspace service keys.

Create a service key

Only a Tenant Admin can create or revoke a service key. Everyone else sees the tab read-only, with the note “Workspace service keys are created and revoked by Tenant Admins.”, and no create or revoke action.

  1. Open Settings → API keys and choose the Workspace service keys tab.
  2. Click New service key.
  3. Project comes first. It defaults to the project you last worked in (or the first open project). Pick another, or pick All projects — see project binding.
  4. Give it a Label that names the pipeline and the job (required, up to 120 characters), for example GitHub Actions — e2e (main).
  5. Choose Scopes. ingest:results is always on and cannot be unticked; tick read if the job also needs to list its projects.
  6. Choose when it Expires: 90 days, 1 year (the default) or Never expires (marked Not recommended).
  7. Click Create service key, or press Enter in the label.

The Service key created dialog shows the key once, with its project, its scopes and how CI should carry it. Like the personal-key reveal, it cannot be dismissed by accident: clicking outside it and Escape do nothing. Copy and close copies the key and closes the dialog (if the browser blocks the clipboard, it stays open and says so); I’ve saved it elsewhere closes it once you have stored the key yourself. After that only the prefix (to_svc_…) is ever shown again.

Store the key as a CI secret (for example TO_API_KEY), never in the repository. Anyone holding it can upload results into the key’s project — or every project, for an all-projects key.

The list shows each key’s label and who created it, its project, prefix, scopes, status (Active, Expired or Revoked), when it last authenticated a request (Never until a job uses it) and when it expires — flagged once expiry is within two weeks.

Scopes

ScopeLets the keyHeld by
ingest:resultsCall POST /api/v1/ingest/results. It cannot read anything.Every service key — a service key without it cannot be created
readCall GET /api/v1/projects, which lists the projects the key can act on with their prj_ ids.Service keys you tick it on
mcpUse the MCP server as the key’s owner.Personal keys only — never a service key

A key that calls a route without its scope gets 403 SCOPE_REQUIRED. Each route’s scope is listed in the API reference (x-required-scope).

Project binding

A project is named on the public API by its public id: prj_ followed by 12 characters, for example prj_8kd2hq0vxc1a. It never changes and is safe to keep in CI config. The numeric id in the app URL is never accepted.

  • A key for one project (the default) uploads into that project. The request needs no project field. If you send one anyway, it must be that key’s own project; any other value answers 403 KEY_PROJECT_MISMATCH.
  • An all-projects key uploads into any project of the workspace, so every upload must say which: project=prj_…. Without it the answer is 400 PROJECT_REQUIRED; a project of another workspace, or one that does not exist, is 403 KEY_PROJECT_MISMATCH. The create dialog warns before you choose it: prefer one key per project unless one pipeline reports for several.
  • A project value that is not a prj_ id — a numeric id, a name — answers 400 INVALID_PROJECT_ID.

Finding a prj_ id. Call GET /api/v1/projects with a key that has the read scope: a project key lists its one project, an all-projects key lists every project in the workspace. MCP clients see the same id as publicId in get_projects.

Plan limits

Each plan includes a number of active service keys. Revoked and expired keys do not count, and personal keys never count.

PlanActive service keys
Free1 active service key
Starter5 active service keys
Pro25 active service keys
BusinessNo limit

At the limit, the tab says so and names the next plan, and New service key is disabled until a key is revoked. If another admin used the last slot while your dialog was open, creating answers 403 PLAN_LIMIT_SERVICE_KEYS with limit and requiredPlan, and the dialog shows the same notice without losing what you typed.

Rotation and revocation

There is no in-place rotation and no way to reactivate a key. To rotate, create a new key, replace the CI secret, confirm the job authenticates (its Last used updates), then revoke the old key.

  • Revoke from the key’s row menu: Revoke key…. A key that authenticated a request in the last seven days asks you to type REVOKE, because a pipeline is using it; a dormant key asks for a plain confirmation. Revocation is immediate.
  • A revoked or expired key answers 401 API_KEY_UNAUTHORIZED on every request. Expired and revoked keys stay on the list, greyed, with the reason on hover; use the Status filter to hide them.
  • Only a Tenant Admin can revoke a service key.

CI quick start

Send the results file as multipart/form-data to https://api.testorchestrator.com/api/v1/ingest/results with the key as a bearer token. file and format (junit-xml, playwright-json or to-results-json) are required; project is only for an all-projects key. Every field, with its limits, is in the API reference.

curl

curl -sS -X POST https://api.testorchestrator.com/api/v1/ingest/results \
  -H "Authorization: Bearer $TO_API_KEY" \
  -H "Idempotency-Key: $CI_RUN_ID-$CI_RUN_ATTEMPT" \
  -F file=@results/junit.xml \
  -F format=junit-xml \
  -F run_key=ci-$CI_RUN_ID \
  -F ci_branch=main

A successful upload answers 202:

{ "ingest_id": "ing_7f3k9q2m4x8z1c5v", "status": "accepted" }

GitHub Actions

# .github/workflows/e2e.yml — store the key as the repository secret TO_API_KEY
- name: Run Playwright
  run: npx playwright test --reporter=json > results.json
- name: Report to TestOrchestrator
  if: always()
  run: |
    curl -sS --fail-with-body -X POST https://api.testorchestrator.com/api/v1/ingest/results \
      -H "Authorization: Bearer ${{ secrets.TO_API_KEY }}" \
      -H "Idempotency-Key: ${{ github.run_id }}-${{ github.run_attempt }}" \
      -F file=@results.json \
      -F format=playwright-json \
      -F run_key=gha-${{ github.run_id }} \
      -F ci_branch=${{ github.ref_name }} \
      -F ci_commit=${{ github.sha }} \
      -F ci_provider=github

With an all-projects key, add -F project=prj_… to each upload.

Retries and rate limits

  • Idempotency. Send an Idempotency-Key header (1–255 visible characters, for example the CI run id plus the attempt). Repeating the same upload with the same key within 24 hours returns the first answer again with Idempotent-Replay: true and writes nothing twice. The same key with a different upload answers 422 IDEMPOTENCY_KEY_REUSED.
  • Rate limit. Each key has its own token bucket of 60 uploads that refills at one per second: a burst of up to 60, then 60 a minute. Over it, the answer is 429 RATE_LIMIT_EXCEEDED with Retry-After in seconds.
  • Tenant headers. The key alone decides the workspace. Do not send X-Tenant-Workspace or X-Tenant-Subdomain with it.

Troubleshooting

Every refusal has the body {errorCode, message, status, details, path, timestamp}.

ResponseCauseFix
401 API_KEY_UNAUTHORIZEDNo key, an unknown key, or a revoked or expired one.Check the CI secret, and the key’s status and expiry on the Workspace service keys tab.
403 SCOPE_REQUIREDThe key lacks the route’s scope: a personal key on /api/v1, or a service key without read on GET /api/v1/projects.Use a service key; create one with read if the job lists projects.
403 KEY_PROJECT_MISMATCHThe project sent is not this key’s project, belongs to another workspace, or does not exist.Use the key created for that project, or check the prj_ id with GET /api/v1/projects.
400 PROJECT_REQUIREDAn all-projects key uploaded without project.Add -F project=prj_…, or use a project key.
400 INVALID_PROJECT_IDproject is not a prj_ id — for example a numeric id.Pass the public id, never the number from the app URL.
400 TENANT_HEADER_NOT_ALLOWEDA tenant header was sent with a key.Remove X-Tenant-Workspace / X-Tenant-Subdomain.
400 INVALID_FORMATformat is missing or not one of the three names.Use junit-xml, playwright-json or to-results-json.
400 FILE_REQUIREDThe request is not multipart, or has no file part.Send -F file=@…; an empty file is allowed.
400 INVALID_PARAMETERA field is malformed or too long; details names it.Check the field’s limit in the API reference.
400 IDEMPOTENCY_KEY_INVALIDThe Idempotency-Key header is empty, too long or has invisible characters.Use 1–255 visible characters.
422 IDEMPOTENCY_KEY_REUSEDThe same Idempotency-Key named a different upload in the last 24 hours.Make the key unique per upload (run id, attempt, shard).
413 FILE_TOO_LARGEThe file is over 25 MB.Split it by shard, or drop stdout from the report.
429 RATE_LIMIT_EXCEEDEDThe key’s bucket is empty (a burst of 60, refilling at one per second).Wait for Retry-After; upload once per CI job rather than per spec file.
503 INGEST_NOT_AVAILABLEResult upload is switched off — it opens in a following release.Keep the key; retry after Retry-After once upload opens.
403 SERVICE_KEY_ADMIN_ONLYSomeone other than a Tenant Admin tried to create a service key.Ask a Tenant Admin.
403 PLAN_LIMIT_SERVICE_KEYSThe plan’s active service keys are all in use; the body names limit and requiredPlan.Revoke a key you no longer use, or move to the next plan.

Was this page helpful?Tell us what’s missingOpens an email to support with the page already named.