{"openapi":"3.1.0","info":{"title":"Outqube API","version":"1","description":"Start test runs, read their results and evidence, and manage an app's event webhooks and hooks. Every API key belongs to one app and has one scope: read, trigger, manage (each includes the ones before it). Send it as `Authorization: Bearer $OUTQUBE_API_KEY`. Lists are paged with limit and cursor, every error has the same shape, and the run events Outqube sends to your webhook endpoints are described under webhooks."},"servers":[{"url":"https://app.outqube.com/api/v1","description":"Production"}],"security":[{"apiKey":[]}],"tags":[{"name":"Runs","description":"A run is one start of a suite, a list of tests or a tag. Start, list, cancel and rerun runs, and poll one until its status is completed or cancelled."},{"name":"Results","description":"What a test of a run did: its failure report (Markdown and structured), its steps in order, and its console and network entries. Screenshots and videos are artifacts."},{"name":"Tests","description":"The tests of the app the API key belongs to, with the goal, steps and expected results the agent follows."},{"name":"Suites","description":"Named lists of tests that run together. Start one with createRun."},{"name":"Artifacts","description":"Screenshots and videos kept as proof of a step. An artifact id from a step resolves to a short-lived signed URL."},{"name":"Webhooks","description":"Manage the HTTPS endpoints that receive an app's run events. All of these need a key with the manage scope. The events themselves are described under Webhook events."},{"name":"Webhook events","description":"Outqube sends these messages to your endpoint (add one with createWebhook). Each is an HTTP POST with a JSON body, sent from Outqube's servers. They are listed in the webhooks map of this document because you implement the receiving side.\n\n**Subscribing.** An endpoint gets the event types in its events list, or all of them with `[\"*\"]`. The ping test message is never part of a subscription: it is sent only when you call testWebhook. An app can have 10 endpoints at most. The URL must be https, on a public host and a standard port.\n\n**Headers.** Every message carries `webhook-id` (the message id: the same on every retry, so use it to drop duplicates), `webhook-timestamp` (Unix seconds when this attempt was signed), `webhook-signature`, `content-type: application/json` and `user-agent: Outqube-Webhooks/1`.\n\n**Verifying.** Messages are signed as [Standard Webhooks](https://www.standardwebhooks.com) defines: an HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with the endpoint's secret (`whsec_` followed by the base64 of 32 random bytes; decode the base64 part to get the key) and sent base64-encoded as `v1,<signature>`. Verify the raw request body, before parsing it, with any Standard Webhooks library, and reject a timestamp far from the current time. For 24 hours after a secret rotation the header holds two signatures, one per secret, separated by a space: accept the message if either matches.\n\n**Answering.** Reply with any 2xx status within 10 seconds. Anything else is a failed attempt: another status, no answer in time, a connection error, or a redirect (redirects are not followed).\n\n**Retrying.** A failed message is sent again up to 8 times, after 5 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, each wait varying by up to 20% either way: 9 attempts over about 10.7 hours. The ping test message is sent once and not retried.\n\n**Disabling.** A message that runs out of retries counts against the endpoint, and any delivery resets the count. After 5 such messages in a row Outqube switches the endpoint off (enabled false, disabled_reason failures). Turn it on again with updateWebhook.\n\n**Order and duplicates.** Delivery is at least once and not ordered: a retried older event can arrive after a newer one, and the same event can arrive twice. Drop duplicates by webhook-id, use the payload's timestamp and status to order events, and fetch the run from api_url when order matters."},{"name":"Hooks","description":"The setup and teardown hooks of an app: HTTPS URLs Outqube calls before a run's first test and after its last. All of these need a key with the manage scope."},{"name":"Meta","description":"The API itself: the OpenAPI document that describes it."}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"oq_ followed by 43 letters and digits","description":"An Outqube API key, made in the app's settings. It belongs to one app and has the scope read, trigger or manage; a higher scope includes the lower ones. A revoked or expired key is a 401."}},"headers":{"RequestId":{"description":"Identifies the request; also in error bodies. Quote it when asking for help.","schema":{"type":"string","format":"uuid"}},"RetryAfter":{"description":"Seconds to wait before trying again.","schema":{"type":"integer","minimum":1}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["invalid_request","unauthorized","forbidden","not_found","method_not_allowed","conflict","idempotency_mismatch","invalid_base_url","no_tests","unprocessable","rate_limited","internal","unavailable"],"description":"A stable code to branch on, with its HTTP status: invalid_request (400), unauthorized (401), forbidden (403), not_found (404), method_not_allowed (405), conflict (409), idempotency_mismatch (422), invalid_base_url (422), no_tests (422), unprocessable (422), rate_limited (429), internal (500), unavailable (503)."},"message":{"type":"string","description":"A short explanation for people. The wording can change: do not match on it."},"request_id":{"type":"string","description":"Identifies the request; also in the request-id header. Quote it for help."}},"required":["code","message","request_id"],"additionalProperties":false,"description":"What went wrong."}},"required":["error"],"additionalProperties":false,"description":"Every error the API answers has this shape."},"Hooks":{"type":"object","properties":{"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The app the hooks belong to."},"setup_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Called before a run's first test; null for none."},"teardown_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Called after a run's last test; null for none."},"timeout_seconds":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Seconds to wait for an answer: 1 to 120."},"enabled":{"type":"boolean","description":"Whether Outqube calls the hooks."},"secret_hint":{"type":"string","description":"The last 4 characters of the signing secret."},"previous_secret_expires_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"After a rotation, when the previous secret stops signing; else null."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the hooks were first saved."},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the hooks last changed."}},"required":["app_id","setup_url","teardown_url","timeout_seconds","enabled","secret_hint","previous_secret_expires_at","created_at","updated_at"],"additionalProperties":false,"description":"An app's setup and teardown hooks."},"HooksSaved":{"type":"object","properties":{"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The app the hooks belong to."},"setup_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Called before a run's first test; null for none."},"teardown_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Called after a run's last test; null for none."},"timeout_seconds":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Seconds to wait for an answer: 1 to 120."},"enabled":{"type":"boolean","description":"Whether Outqube calls the hooks."},"secret_hint":{"type":"string","description":"The last 4 characters of the signing secret."},"previous_secret_expires_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"After a rotation, when the previous secret stops signing; else null."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the hooks were first saved."},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the hooks last changed."},"secret":{"description":"The signing secret, only on the save that made it (the app's first); else null.","type":["string","null"]}},"required":["app_id","setup_url","teardown_url","timeout_seconds","enabled","secret_hint","previous_secret_expires_at","created_at","updated_at","secret"],"additionalProperties":false,"description":"The hooks as saved; the signing secret comes with the app's first save only."},"HooksWithSecret":{"type":"object","properties":{"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The app the hooks belong to."},"setup_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Called before a run's first test; null for none."},"teardown_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Called after a run's last test; null for none."},"timeout_seconds":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Seconds to wait for an answer: 1 to 120."},"enabled":{"type":"boolean","description":"Whether Outqube calls the hooks."},"secret_hint":{"type":"string","description":"The last 4 characters of the signing secret."},"previous_secret_expires_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"After a rotation, when the previous secret stops signing; else null."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the hooks were first saved."},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the hooks last changed."},"secret":{"type":"string","description":"The signing secret, `whsec_` + base64. Shown here and in settings."}},"required":["app_id","setup_url","teardown_url","timeout_seconds","enabled","secret_hint","previous_secret_expires_at","created_at","updated_at","secret"],"additionalProperties":false,"description":"The hooks with their signing secret, returned on rotate."},"LogEntry":{"type":"object","properties":{"step":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"The step the entry belongs to, or null."},"kind":{"type":"string","enum":["console","network"],"description":"console for browser console messages, network for requests."},"level":{"description":"error, warning or info for console messages; null for network entries.","type":["string","null"]},"message":{"type":"string","description":"The console message, or a summary of the request."},"url":{"description":"The request URL for network entries; else null.","type":["string","null"]},"method":{"description":"The HTTP method for network entries; else null.","type":["string","null"]},"status":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"The HTTP status for network entries; else null."},"at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the entry was recorded."}},"required":["step","kind","level","message","url","method","status","at"],"additionalProperties":false,"description":"One console message or network request seen during a test."},"PingEvent":{"type":"object","properties":{"type":{"type":"string","const":"ping","description":"Always ping."},"timestamp":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the ping was made (ISO 8601, UTC)."},"data":{"type":"object","properties":{"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$","description":"The app the endpoint belongs to."},"webhook_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$","description":"The endpoint's id."}},"required":["app_id","webhook_id"],"additionalProperties":false,"description":"Which endpoint the ping is for."}},"required":["type","timestamp","data"],"additionalProperties":false,"description":"The body of a ping test message."},"Run":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The run's id."},"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The app the run belongs to."},"status":{"anyOf":[{"type":"string","enum":["queued","running","completed","cancelled"]},{"type":"string"}],"description":"queued, running, completed or cancelled; it never goes back. More values may be added: treat an unknown one as not finished."},"outcome":{"anyOf":[{"anyOf":[{"type":"string","enum":["passed","failed","error"]},{"type":"string"}],"description":"What a finished run says about the app: passed, failed (a verdict against the app) or error (anything that is not a verdict). Null while running and when cancelled. More values may be added: treat an unknown one as not passed."},{"type":"null"}],"description":"What a finished run says about the app: passed, failed (a verdict against the app) or error (anything that is not a verdict). Null while running and when cancelled. More values may be added: treat an unknown one as not passed."},"counts":{"type":"object","properties":{"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs in the run: one per test per device."},"queued":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs waiting to start."},"running":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs in progress."},"passed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs that passed."},"failed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs with a verdict against the app."},"error":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs that ended without a verdict: infrastructure, setup or blocked."},"cancelled":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs that were stopped."}},"required":["total","queued","running","passed","failed","error","cancelled"],"additionalProperties":false,"description":"Test runs by state. A retried test counts its final attempt only."},"metadata":{"type":"object","properties":{"source":{"anyOf":[{"type":"string","enum":["api","cli","github","schedule","ui"]},{"type":"string"}],"description":"Where the run was started: api, cli, github, schedule or ui. More values may be added."},"commit_sha":{"description":"The commit under test, if one was sent.","type":["string","null"]},"branch":{"description":"The branch under test, if one was sent.","type":["string","null"]},"pr_number":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"The pull request number, if one was sent."},"repo":{"description":"owner/name or the CI project path, if sent.","type":["string","null"]}},"required":["source","commit_sha","branch","pr_number","repo"],"additionalProperties":false,"description":"Where the run came from."},"suite_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The suite the run started, or null."},"environment_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The environment the run used, or null."},"base_url":{"description":"The base_url override, or null for none.","type":["string","null"]},"rerun_of":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The run this one reruns, or null."},"url":{"type":"string","format":"uri","description":"The run's page in the Outqube app."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the run was created."},"started_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"When the first test started, or null."},"completed_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"When the run ended, or null."},"duration_ms":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Milliseconds from start to end, or null."}},"required":["id","app_id","status","outcome","counts","metadata","suite_id","environment_id","base_url","rerun_of","url","created_at","started_at","completed_at","duration_ms"],"additionalProperties":false,"description":"One start of a suite, a list of tests or a tag. GET /runs/{runId} adds its tests."},"RunDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The run's id."},"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The app the run belongs to."},"status":{"anyOf":[{"type":"string","enum":["queued","running","completed","cancelled"]},{"type":"string"}],"description":"queued, running, completed or cancelled; it never goes back. More values may be added: treat an unknown one as not finished."},"outcome":{"anyOf":[{"anyOf":[{"type":"string","enum":["passed","failed","error"]},{"type":"string"}],"description":"What a finished run says about the app: passed, failed (a verdict against the app) or error (anything that is not a verdict). Null while running and when cancelled. More values may be added: treat an unknown one as not passed."},{"type":"null"}],"description":"What a finished run says about the app: passed, failed (a verdict against the app) or error (anything that is not a verdict). Null while running and when cancelled. More values may be added: treat an unknown one as not passed."},"counts":{"type":"object","properties":{"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs in the run: one per test per device."},"queued":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs waiting to start."},"running":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs in progress."},"passed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs that passed."},"failed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs with a verdict against the app."},"error":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs that ended without a verdict: infrastructure, setup or blocked."},"cancelled":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Test runs that were stopped."}},"required":["total","queued","running","passed","failed","error","cancelled"],"additionalProperties":false,"description":"Test runs by state. A retried test counts its final attempt only."},"metadata":{"type":"object","properties":{"source":{"anyOf":[{"type":"string","enum":["api","cli","github","schedule","ui"]},{"type":"string"}],"description":"Where the run was started: api, cli, github, schedule or ui. More values may be added."},"commit_sha":{"description":"The commit under test, if one was sent.","type":["string","null"]},"branch":{"description":"The branch under test, if one was sent.","type":["string","null"]},"pr_number":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"The pull request number, if one was sent."},"repo":{"description":"owner/name or the CI project path, if sent.","type":["string","null"]}},"required":["source","commit_sha","branch","pr_number","repo"],"additionalProperties":false,"description":"Where the run came from."},"suite_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The suite the run started, or null."},"environment_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The environment the run used, or null."},"base_url":{"description":"The base_url override, or null for none.","type":["string","null"]},"rerun_of":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The run this one reruns, or null."},"url":{"type":"string","format":"uri","description":"The run's page in the Outqube app."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the run was created."},"started_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"When the first test started, or null."},"completed_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"When the run ended, or null."},"duration_ms":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Milliseconds from start to end, or null."},"tests":{"type":"array","items":{"$ref":"#/components/schemas/TestRun"},"description":"The run's tests, at most 100."}},"required":["id","app_id","status","outcome","counts","metadata","suite_id","environment_id","base_url","rerun_of","url","created_at","started_at","completed_at","duration_ms","tests"],"additionalProperties":false,"description":"A run with its tests."},"RunEvent":{"type":"object","properties":{"type":{"type":"string","enum":["run.queued","run.started","run.needs_attention","run.completed","run.cancelled"],"description":"The event type."},"timestamp":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the event happened (ISO 8601, UTC)."},"data":{"type":"object","properties":{"run":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$","description":"The run's id; GET /runs/{runId} returns it."},"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$","description":"The app the run belongs to."},"status":{"type":"string","enum":["queued","running","completed","cancelled"],"description":"queued, running, completed or cancelled, as of this event."},"outcome":{"anyOf":[{"type":"string","enum":["passed","failed","error"]},{"type":"null"}],"description":"passed, failed or error once completed; null before and when cancelled."},"source":{"type":"string","enum":["api","cli","github","schedule","ui"],"description":"Where the run was started: api, cli, github, schedule or ui."},"commit_sha":{"description":"The commit under test, if one was sent.","type":["string","null"]},"branch":{"description":"The branch under test, if one was sent.","type":["string","null"]},"pr_number":{"anyOf":[{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},{"type":"null"}],"description":"The pull request number, if one was sent."},"repo":{"description":"owner/name or the CI project path, if sent.","type":["string","null"]},"counts":{"type":"object","properties":{"total":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Test runs in the run, including ones still to finish or stopped."},"passed":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Test runs that passed."},"failed":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Test runs with a verdict against the app."},"error":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Test runs that ended without a verdict."}},"required":["total","passed","failed","error"],"additionalProperties":false,"description":"Test runs by final outcome. total counts every test run, including ones still to finish or stopped."},"url":{"type":"string","format":"uri","description":"The run's page in the Outqube app."},"api_url":{"type":"string","format":"uri","description":"The run's resource in this API: fetch it for the details."}},"required":["id","app_id","status","outcome","source","commit_sha","branch","pr_number","repo","counts","url","api_url"],"additionalProperties":false,"description":"The run, without its tests, steps or logs."},"test_run":{"description":"run.needs_attention only: the test run waiting for a person.","type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$","description":"The test run that is waiting."},"test_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},{"type":"null"}],"description":"The test it runs, or null if the test was deleted."},"status":{"type":"string","enum":["waiting_for_input"],"description":"Always waiting_for_input."}},"required":["id","test_id","status"],"additionalProperties":false}},"required":["run"],"additionalProperties":false,"description":"What the event is about."}},"required":["type","timestamp","data"],"additionalProperties":false,"description":"The body of a run event message. It is thin on purpose: fetch details from api_url."},"RunMetadata":{"type":"object","properties":{"source":{"description":"api (the default) or cli. github, schedule and ui are set by Outqube itself: sending one is a 400.","type":"string","enum":["api","cli","github","schedule","ui"]},"commit_sha":{"description":"The commit under test; for a pull request, the head commit, not the merge commit.","type":"string","pattern":"^[0-9a-fA-F]{7,64}$"},"branch":{"description":"The branch under test.","type":"string","minLength":1,"maxLength":250},"pr_number":{"description":"The pull request or merge request number.","type":"integer","exclusiveMinimum":0,"maximum":2147483647},"repo":{"description":"owner/name, or the CI project path.","type":"string","minLength":1,"maxLength":200}},"description":"Where a run came from. Every field is optional; send what your CI knows."},"StepDetail":{"type":"object","properties":{"n":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The step's number in the test, from 1."},"action":{"type":"string","description":"What the agent did, for example click, type or navigate."},"summary":{"type":"string","description":"The step in plain words."},"url":{"description":"The page the step happened on, or null.","type":["string","null"]},"outcome":{"anyOf":[{"type":"string","enum":["ok","error","blocked"]},{"type":"string"}],"description":"ok, error, or blocked (the page stopped the agent, for example a captcha). More values may be added."},"at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the step happened."},"reasoning":{"type":"string","description":"Why the agent took this step."},"console":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"},"description":"Console messages from this step."},"network":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"},"description":"Network requests from this step."},"screenshot_artifact_id":{"anyOf":[{"type":"string","pattern":"^art_[A-Za-z0-9_-]{4,200}$"},{"type":"null"}],"description":"Resolve with GET /artifacts/{artifactId}; null when the evidence is gone (retention)."}},"required":["n","action","summary","url","outcome","at","reasoning","console","network","screenshot_artifact_id"],"additionalProperties":false,"description":"One step with its reasoning, console and network entries, and screenshot."},"StepSummary":{"type":"object","properties":{"n":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The step's number in the test, from 1."},"action":{"type":"string","description":"What the agent did, for example click, type or navigate."},"summary":{"type":"string","description":"The step in plain words."},"url":{"description":"The page the step happened on, or null.","type":["string","null"]},"outcome":{"anyOf":[{"type":"string","enum":["ok","error","blocked"]},{"type":"string"}],"description":"ok, error, or blocked (the page stopped the agent, for example a captcha). More values may be added."},"at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the step happened."}},"required":["n","action","summary","url","outcome","at"],"additionalProperties":false,"description":"One step of a test: what the agent did."},"Suite":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The suite's id."},"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The app the suite belongs to."},"name":{"type":"string","description":"The suite's name; createRun accepts it as suite."},"description":{"type":"string","description":"What the suite is for; empty when not set."},"default_environment_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The environment a run of the suite uses unless one is given, or null."},"test_count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many tests the suite holds."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the suite was created."},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the suite last changed."}},"required":["id","app_id","name","description","default_environment_id","test_count","created_at","updated_at"],"additionalProperties":false,"description":"A named list of tests that run together."},"Test":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The test's id."},"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The app the test belongs to."},"number":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The test's number in the app."},"display_id":{"type":"string","description":"The test's number as the app shows it, for example \"TEST-012\"."},"name":{"type":"string","description":"The test's name."},"goal":{"type":"string","description":"What the test checks: its intent."},"priority":{"type":"string","enum":["urgent","high","medium","low"],"description":"urgent, high, medium or low."},"tags":{"type":"array","items":{"type":"string"},"description":"The test's tags; createRun can run every test with one."},"devices":{"type":"array","items":{"type":"string","enum":["desktop","iphone-15-pro","iphone-se-3","ipad-mini","ipad-pro-11"]},"description":"The devices the test runs on."},"environment_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The environment the test is pinned to, or null for the suite's or app's."},"suite_ids":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"description":"The suites the test is in."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the test was created."},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the test last changed."}},"required":["id","app_id","number","display_id","name","goal","priority","tags","devices","environment_id","suite_ids","created_at","updated_at"],"additionalProperties":false,"description":"A test of the app."},"TestDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The test's id."},"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The app the test belongs to."},"number":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The test's number in the app."},"display_id":{"type":"string","description":"The test's number as the app shows it, for example \"TEST-012\"."},"name":{"type":"string","description":"The test's name."},"goal":{"type":"string","description":"What the test checks: its intent."},"priority":{"type":"string","enum":["urgent","high","medium","low"],"description":"urgent, high, medium or low."},"tags":{"type":"array","items":{"type":"string"},"description":"The test's tags; createRun can run every test with one."},"devices":{"type":"array","items":{"type":"string","enum":["desktop","iphone-15-pro","iphone-se-3","ipad-mini","ipad-pro-11"]},"description":"The devices the test runs on."},"environment_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The environment the test is pinned to, or null for the suite's or app's."},"suite_ids":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"description":"The suites the test is in."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the test was created."},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the test last changed."},"instruction":{"type":"string","description":"The steps, as plain text the agent follows."},"expected_results":{"type":"array","items":{"type":"string"},"description":"What must be true for the test to pass."},"special_instructions":{"type":"string","description":"Extra guidance for the agent."}},"required":["id","app_id","number","display_id","name","goal","priority","tags","devices","environment_id","suite_ids","created_at","updated_at","instruction","expected_results","special_instructions"],"additionalProperties":false,"description":"A test with the steps, expected results and instructions the agent follows."},"TestReport":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","const":"ready","description":"The report is written."},"report":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The structured report: what failed, evidence and reproduction steps."},"markdown":{"type":"string","description":"The same report as Markdown, at most about 2,000 tokens."}},"required":["status","report","markdown"],"additionalProperties":false},{"type":"object","properties":{"status":{"type":"string","const":"pending","description":"The report is not written yet."}},"required":["status"],"additionalProperties":false},{"type":"object","properties":{"status":{"type":"string","const":"failed","description":"The report could not be written."}},"required":["status"],"additionalProperties":false}],"description":"A failed or errored test's failure report, written once when the test ends."},"TestRun":{"type":"object","properties":{"test_run_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The id the report, steps and logs routes take."},"test_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The test that ran."},"name":{"type":"string","description":"The test's name."},"device":{"description":"The device the test ran on, for example \"desktop\" or \"iphone-15-pro\"; null when not set.","type":["string","null"]},"status":{"anyOf":[{"type":"string","enum":["queued","running","completed","cancelled"]},{"type":"string"}],"description":"queued, running, completed or cancelled. More values may be added."},"outcome":{"anyOf":[{"anyOf":[{"type":"string","enum":["passed","failed","error"]},{"type":"string"}],"description":"passed, failed or error; null while running and when cancelled. More values may be added."},{"type":"null"}],"description":"passed, failed or error; null while running and when cancelled. More values may be added."},"outcome_detail":{"description":"The finer verdict behind an outcome, for example blocked or inconclusive.","type":["string","null"]},"duration_ms":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Milliseconds from start to end, or null."}},"required":["test_run_id","test_id","name","device","status","outcome","outcome_detail","duration_ms"],"additionalProperties":false,"description":"One test of a run, on one device."},"Webhook":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The endpoint's id."},"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The app the endpoint belongs to."},"url":{"type":"string","format":"uri","description":"Where Outqube sends the events."},"events":{"type":"array","items":{"type":"string"},"description":"Event types sent, or [\"*\"] for all of them."},"description":{"type":"string","description":"Your note on what the endpoint is for; empty when not set."},"enabled":{"type":"boolean","description":"Whether Outqube sends events to the endpoint."},"disabled_reason":{"anyOf":[{"type":"string","enum":["manual","failures"]},{"type":"null"}],"description":"manual: switched off by a person. failures: 5 messages in a row ran out of retries. Null while enabled."},"disabled_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"When it was switched off, or null."},"consecutive_failures":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Messages in a row that ran out of retries; any delivery resets it."},"secret_hint":{"type":"string","description":"The last 4 characters of the signing secret."},"previous_secret_expires_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"After a rotation, when the previous secret stops signing; else null."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the endpoint was created."},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the endpoint last changed."}},"required":["id","app_id","url","events","description","enabled","disabled_reason","disabled_at","consecutive_failures","secret_hint","previous_secret_expires_at","created_at","updated_at"],"additionalProperties":false,"description":"An HTTPS endpoint that receives the app's run events."},"WebhookWithSecret":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The endpoint's id."},"app_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The app the endpoint belongs to."},"url":{"type":"string","format":"uri","description":"Where Outqube sends the events."},"events":{"type":"array","items":{"type":"string"},"description":"Event types sent, or [\"*\"] for all of them."},"description":{"type":"string","description":"Your note on what the endpoint is for; empty when not set."},"enabled":{"type":"boolean","description":"Whether Outqube sends events to the endpoint."},"disabled_reason":{"anyOf":[{"type":"string","enum":["manual","failures"]},{"type":"null"}],"description":"manual: switched off by a person. failures: 5 messages in a row ran out of retries. Null while enabled."},"disabled_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"When it was switched off, or null."},"consecutive_failures":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Messages in a row that ran out of retries; any delivery resets it."},"secret_hint":{"type":"string","description":"The last 4 characters of the signing secret."},"previous_secret_expires_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"After a rotation, when the previous secret stops signing; else null."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the endpoint was created."},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the endpoint last changed."},"secret":{"type":"string","description":"The signing secret, `whsec_` + base64. Shown here and in settings."}},"required":["id","app_id","url","events","description","enabled","disabled_reason","disabled_at","consecutive_failures","secret_hint","previous_secret_expires_at","created_at","updated_at","secret"],"additionalProperties":false,"description":"An endpoint with its signing secret, returned only on create and on rotate."}}},"paths":{"/tests":{"get":{"operationId":"listTests","summary":"List the app's tests, newest first","description":"Pages of the app's tests, newest first, without their steps; use getTest for those. Follow next_cursor until it is null.\n\nNeeds an API key with the read scope.","tags":["Tests"],"security":[{"apiKey":[]}],"x-outqube-scope":"read","parameters":[{"name":"limit","in":"query","required":false,"description":"Items per page, 1 to 100.","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"The next_cursor of the previous page. Opaque: pass it back as given.","schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"tag","in":"query","required":false,"description":"Only tests with this tag (exact, up to 40 characters).","schema":{"type":"string","minLength":1,"maxLength":40,"pattern":"^[^\\p{Cc}]+$"}}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Test"},"description":"The items of this page."},"next_cursor":{"description":"Pass as cursor to get the next page; null on the last page.","type":["string","null"]}},"required":["data","next_cursor"],"additionalProperties":false},"examples":{"default":{"summary":"Example response","value":{"data":[{"id":"5d7e9a1c-3b2f-4c84-a6e0-1f8b3d5c7a92","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","number":12,"display_id":"TEST-012","name":"Sign in with a valid account","goal":"A registered user can sign in and lands on the dashboard.","priority":"high","tags":["smoke","auth"],"devices":["desktop"],"environment_id":"1a3c5e7b-9d2f-4b46-a8c0-4e6a8c0d2f57","suite_ids":["7c2a4e6b-8d1f-4a35-b97c-2e4a6c8d0f13"],"created_at":"2026-10-01T09:30:00.000Z","updated_at":"2026-10-20T14:12:00.000Z"}],"next_cursor":null}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/tests/{testId}":{"get":{"operationId":"getTest","summary":"Get one test","description":"One test with its steps (instruction), expected results and special instructions.\n\nNeeds an API key with the read scope.","tags":["Tests"],"security":[{"apiKey":[]}],"x-outqube-scope":"read","parameters":[{"name":"testId","in":"path","required":true,"description":"The test's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"5d7e9a1c-3b2f-4c84-a6e0-1f8b3d5c7a92"}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestDetail"},"examples":{"default":{"summary":"Example response","value":{"id":"5d7e9a1c-3b2f-4c84-a6e0-1f8b3d5c7a92","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","number":12,"display_id":"TEST-012","name":"Sign in with a valid account","goal":"A registered user can sign in and lands on the dashboard.","priority":"high","tags":["smoke","auth"],"devices":["desktop"],"environment_id":"1a3c5e7b-9d2f-4b46-a8c0-4e6a8c0d2f57","suite_ids":["7c2a4e6b-8d1f-4a35-b97c-2e4a6c8d0f13"],"created_at":"2026-10-01T09:30:00.000Z","updated_at":"2026-10-20T14:12:00.000Z","instruction":"Open the sign-in page. Enter the test account's email and password. Select Sign in.","expected_results":["The dashboard opens.","The account name shows in the top bar."],"special_instructions":"Dismiss the cookie banner if it appears."}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/suites":{"get":{"operationId":"listSuites","summary":"List the app's suites, newest first","description":"Pages of the app's suites, newest first. Follow next_cursor until it is null.\n\nNeeds an API key with the read scope.","tags":["Suites"],"security":[{"apiKey":[]}],"x-outqube-scope":"read","parameters":[{"name":"limit","in":"query","required":false,"description":"Items per page, 1 to 100.","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"The next_cursor of the previous page. Opaque: pass it back as given.","schema":{"type":"string","minLength":1,"maxLength":200}}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Suite"},"description":"The items of this page."},"next_cursor":{"description":"Pass as cursor to get the next page; null on the last page.","type":["string","null"]}},"required":["data","next_cursor"],"additionalProperties":false},"examples":{"default":{"summary":"Example response","value":{"data":[{"id":"7c2a4e6b-8d1f-4a35-b97c-2e4a6c8d0f13","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","name":"Smoke","description":"The checks to run on every pull request.","default_environment_id":"1a3c5e7b-9d2f-4b46-a8c0-4e6a8c0d2f57","test_count":8,"created_at":"2026-10-01T09:45:00.000Z","updated_at":"2026-10-18T08:05:00.000Z"}],"next_cursor":null}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/runs":{"post":{"operationId":"createRun","summary":"Start a run","description":"Give exactly one of suite, test_ids or tag. A suite or tag runs at most 100 tests; test_ids holds at most 100. The response is the run (status queued) with its url; list its tests with GET /runs/{runId}, which returns at most 100 tests. Send an Idempotency-Key header to make a retry safe. Errors: 409 conflict when a suite or environment name matches more than one, or a request with the same Idempotency-Key is still running; 422 no_tests when the suite or tag has no tests; 422 invalid_base_url when base_url is not on the environment's host or an allowed domain, or is a private address; 422 unprocessable when more than 100 tests match or the organisation's monthly budget is reached; 422 idempotency_mismatch for a reused Idempotency-Key.\n\nNeeds an API key with the trigger scope.","tags":["Runs"],"security":[{"apiKey":[]}],"x-outqube-scope":"trigger","parameters":[{"name":"idempotency-key","in":"header","required":false,"description":"Any unique string. The request hash covers the method, the path parameters and the body: the same key with the same request returns the same run for 24 hours; the same key with another request is a 422 idempotency_mismatch.","schema":{"type":"string","minLength":8,"maxLength":200,"pattern":"^[\\x21-\\x7e]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"suite":{"type":"string","minLength":1,"maxLength":200,"description":"A suite's id or its name."},"base_url":{"description":"Test this URL instead of the environment's. It must be on the environment's host or one of its allowed domains, and not a private address (422 invalid_base_url). A wildcard allowed domain over shared hosting, such as *.vercel.app, doesn't allow it: allow the exact host.","type":"string","maxLength":2000,"format":"uri"},"environment":{"description":"An environment's name or id. A name that matches more than one is a 409 conflict.","type":"string","minLength":1,"maxLength":200},"metadata":{"$ref":"#/components/schemas/RunMetadata"}},"required":["suite"],"additionalProperties":false},{"type":"object","properties":{"test_ids":{"minItems":1,"maxItems":100,"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"description":"The tests to run, at most 100."},"base_url":{"description":"Test this URL instead of the environment's. It must be on the environment's host or one of its allowed domains, and not a private address (422 invalid_base_url). A wildcard allowed domain over shared hosting, such as *.vercel.app, doesn't allow it: allow the exact host.","type":"string","maxLength":2000,"format":"uri"},"environment":{"description":"An environment's name or id. A name that matches more than one is a 409 conflict.","type":"string","minLength":1,"maxLength":200},"metadata":{"$ref":"#/components/schemas/RunMetadata"}},"required":["test_ids"],"additionalProperties":false},{"type":"object","properties":{"tag":{"type":"string","minLength":1,"maxLength":40,"description":"Every test with this tag, at most 100."},"base_url":{"description":"Test this URL instead of the environment's. It must be on the environment's host or one of its allowed domains, and not a private address (422 invalid_base_url). A wildcard allowed domain over shared hosting, such as *.vercel.app, doesn't allow it: allow the exact host.","type":"string","maxLength":2000,"format":"uri"},"environment":{"description":"An environment's name or id. A name that matches more than one is a 409 conflict.","type":"string","minLength":1,"maxLength":200},"metadata":{"$ref":"#/components/schemas/RunMetadata"}},"required":["tag"],"additionalProperties":false}],"description":"Give exactly one of suite, test_ids or tag, and any of base_url, environment and metadata."},"examples":{"default":{"summary":"Example request","value":{"suite":"Smoke","environment":"Staging","metadata":{"source":"api","commit_sha":"4f1c2a9d7e3b","branch":"feature/login","pr_number":42,"repo":"acme/web"}}}}}}},"responses":{"201":{"description":"Created","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Run"},"examples":{"default":{"summary":"Example response","value":{"id":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","status":"queued","outcome":null,"counts":{"total":2,"queued":2,"running":0,"passed":0,"failed":0,"error":0,"cancelled":0},"metadata":{"source":"api","commit_sha":"4f1c2a9d7e3b","branch":"feature/login","pr_number":42,"repo":"acme/web"},"suite_id":"7c2a4e6b-8d1f-4a35-b97c-2e4a6c8d0f13","environment_id":"1a3c5e7b-9d2f-4b46-a8c0-4e6a8c0d2f57","base_url":"https://staging.acme.test","rerun_of":null,"url":"https://app.outqube.com/acme/runs?execution=3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","created_at":"2026-10-30T12:00:00.000Z","started_at":null,"completed_at":null,"duration_ms":null}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"409":{"description":"Conflict with the current state.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"conflict":{"summary":"conflict","value":{"error":{"code":"conflict","message":"That conflicts with the current state.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"422":{"description":"The request is understood but can't be processed.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"no_tests":{"summary":"no_tests","value":{"error":{"code":"no_tests","message":"There are no tests to run.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"invalid_base_url":{"summary":"invalid_base_url","value":{"error":{"code":"invalid_base_url","message":"Use a base URL on the environment's host or one of its allowed domains, not a private address. A wildcard over shared hosting, such as *.vercel.app, doesn't count: allow the exact host.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unprocessable":{"summary":"unprocessable","value":{"error":{"code":"unprocessable","message":"The request can't be processed.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"idempotency_mismatch":{"summary":"idempotency_mismatch","value":{"error":{"code":"idempotency_mismatch","message":"This Idempotency-Key was used with a different request.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}},"get":{"operationId":"listRuns","summary":"List the app's runs, newest first","description":"Pages of runs, newest first. Filters combine with and. Follow next_cursor until it is null.\n\nNeeds an API key with the read scope.","tags":["Runs"],"security":[{"apiKey":[]}],"x-outqube-scope":"read","parameters":[{"name":"limit","in":"query","required":false,"description":"Items per page, 1 to 100.","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"The next_cursor of the previous page. Opaque: pass it back as given.","schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"status","in":"query","required":false,"description":"Only runs in this status.","schema":{"type":"string","enum":["queued","running","completed","cancelled"]}},{"name":"outcome","in":"query","required":false,"description":"Only runs with this outcome.","schema":{"type":"string","enum":["passed","failed","error"]}},{"name":"source","in":"query","required":false,"description":"Only runs started from this source.","schema":{"type":"string","enum":["api","cli","github","schedule","ui"]}},{"name":"branch","in":"query","required":false,"description":"Only runs on this branch (exact).","schema":{"type":"string","minLength":1,"maxLength":250}},{"name":"commit_sha","in":"query","required":false,"description":"Only runs for this commit sha (hexadecimal, 7 to 64 characters).","schema":{"type":"string","pattern":"^[0-9a-fA-F]{7,64}$"}},{"name":"pr_number","in":"query","required":false,"description":"Only runs for this pull request number.","schema":{"type":"integer","exclusiveMinimum":0,"maximum":2147483647}},{"name":"created_after","in":"query","required":false,"description":"Only runs created after this time (ISO 8601, with Z or an offset).","schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Run"},"description":"The items of this page."},"next_cursor":{"description":"Pass as cursor to get the next page; null on the last page.","type":["string","null"]}},"required":["data","next_cursor"],"additionalProperties":false},"examples":{"default":{"summary":"Example response","value":{"data":[{"id":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","status":"completed","outcome":"failed","counts":{"total":2,"queued":0,"running":0,"passed":1,"failed":1,"error":0,"cancelled":0},"metadata":{"source":"api","commit_sha":"4f1c2a9d7e3b","branch":"feature/login","pr_number":42,"repo":"acme/web"},"suite_id":"7c2a4e6b-8d1f-4a35-b97c-2e4a6c8d0f13","environment_id":"1a3c5e7b-9d2f-4b46-a8c0-4e6a8c0d2f57","base_url":"https://staging.acme.test","rerun_of":null,"url":"https://app.outqube.com/acme/runs?execution=3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","created_at":"2026-10-30T12:00:00.000Z","started_at":"2026-10-30T12:00:04.000Z","completed_at":"2026-10-30T12:03:41.000Z","duration_ms":217000}],"next_cursor":null}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/runs/{runId}":{"get":{"operationId":"getRun","summary":"Get a run with its tests (at most 100)","description":"The run with its counts and, in tests, one entry per test and device. Poll this until status is completed or cancelled.\n\nNeeds an API key with the read scope.","tags":["Runs"],"security":[{"apiKey":[]}],"x-outqube-scope":"read","parameters":[{"name":"runId","in":"path","required":true,"description":"The run's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunDetail"},"examples":{"default":{"summary":"Example response","value":{"id":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","status":"completed","outcome":"failed","counts":{"total":2,"queued":0,"running":0,"passed":1,"failed":1,"error":0,"cancelled":0},"metadata":{"source":"api","commit_sha":"4f1c2a9d7e3b","branch":"feature/login","pr_number":42,"repo":"acme/web"},"suite_id":"7c2a4e6b-8d1f-4a35-b97c-2e4a6c8d0f13","environment_id":"1a3c5e7b-9d2f-4b46-a8c0-4e6a8c0d2f57","base_url":"https://staging.acme.test","rerun_of":null,"url":"https://app.outqube.com/acme/runs?execution=3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","created_at":"2026-10-30T12:00:00.000Z","started_at":"2026-10-30T12:00:04.000Z","completed_at":"2026-10-30T12:03:41.000Z","duration_ms":217000,"tests":[{"test_run_id":"c2e5a7b9-1d3f-4a68-b0c4-8e9d2f1a3b57","test_id":"5d7e9a1c-3b2f-4c84-a6e0-1f8b3d5c7a92","name":"Sign in with a valid account","device":"desktop","status":"completed","outcome":"failed","outcome_detail":null,"duration_ms":98000},{"test_run_id":"b9d1f3a5-7c2e-4a84-9e06-3c5e7a9b1d28","test_id":"e1b3d5f7-2a4c-4e69-8b0d-7a9c1e3f5b28","name":"Reset the password","device":"desktop","status":"completed","outcome":"passed","outcome_detail":null,"duration_ms":71000}]}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/runs/{runId}/cancel":{"post":{"operationId":"cancelRun","summary":"Cancel a run (a finished or cancelled run is returned unchanged)","description":"Stops the tests that have not finished. A run that is already completed or cancelled comes back as it is, so calling this twice is safe.\n\nNeeds an API key with the trigger scope.","tags":["Runs"],"security":[{"apiKey":[]}],"x-outqube-scope":"trigger","parameters":[{"name":"runId","in":"path","required":true,"description":"The run's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Run"},"examples":{"default":{"summary":"Example response","value":{"id":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","status":"cancelled","outcome":null,"counts":{"total":2,"queued":0,"running":0,"passed":1,"failed":0,"error":0,"cancelled":1},"metadata":{"source":"api","commit_sha":"4f1c2a9d7e3b","branch":"feature/login","pr_number":42,"repo":"acme/web"},"suite_id":"7c2a4e6b-8d1f-4a35-b97c-2e4a6c8d0f13","environment_id":"1a3c5e7b-9d2f-4b46-a8c0-4e6a8c0d2f57","base_url":"https://staging.acme.test","rerun_of":null,"url":"https://app.outqube.com/acme/runs?execution=3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","created_at":"2026-10-30T12:00:00.000Z","started_at":"2026-10-30T12:00:04.000Z","completed_at":"2026-10-30T12:01:10.000Z","duration_ms":66000}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/runs/{runId}/rerun":{"post":{"operationId":"rerunRun","summary":"Start a new run linked to this one","description":"Starts a new run of the same tests with the same environment, commit and base_url; its rerun_of is this run. Errors: 409 conflict when the run has not finished; 422 no_tests when only_failed is true and every test passed; 422 invalid_base_url, unprocessable and idempotency_mismatch as for createRun.\n\nNeeds an API key with the trigger scope.","tags":["Runs"],"security":[{"apiKey":[]}],"x-outqube-scope":"trigger","parameters":[{"name":"runId","in":"path","required":true,"description":"The run's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"},{"name":"idempotency-key","in":"header","required":false,"description":"Any unique string. The request hash covers the method, the path parameters and the body: the same key with the same request returns the same run for 24 hours; the same key with another request is a 422 idempotency_mismatch.","schema":{"type":"string","minLength":8,"maxLength":200,"pattern":"^[\\x21-\\x7e]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"only_failed":{"default":false,"description":"Rerun only the tests that did not pass.","type":"boolean"}},"description":"Which of the run's tests to rerun."},"examples":{"default":{"summary":"Example request","value":{"only_failed":true}}}}}},"responses":{"201":{"description":"Created","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Run"},"examples":{"default":{"summary":"Example response","value":{"id":"a47c1e92-6d3b-4f50-9b28-3e1d5c7a8f04","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","status":"queued","outcome":null,"counts":{"total":2,"queued":2,"running":0,"passed":0,"failed":0,"error":0,"cancelled":0},"metadata":{"source":"api","commit_sha":"4f1c2a9d7e3b","branch":"feature/login","pr_number":42,"repo":"acme/web"},"suite_id":"7c2a4e6b-8d1f-4a35-b97c-2e4a6c8d0f13","environment_id":"1a3c5e7b-9d2f-4b46-a8c0-4e6a8c0d2f57","base_url":"https://staging.acme.test","rerun_of":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","url":"https://app.outqube.com/acme/runs?execution=3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","created_at":"2026-10-30T13:00:00.000Z","started_at":null,"completed_at":null,"duration_ms":null}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"409":{"description":"Conflict with the current state.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"conflict":{"summary":"conflict","value":{"error":{"code":"conflict","message":"That conflicts with the current state.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"422":{"description":"The request is understood but can't be processed.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"no_tests":{"summary":"no_tests","value":{"error":{"code":"no_tests","message":"There are no tests to run.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"invalid_base_url":{"summary":"invalid_base_url","value":{"error":{"code":"invalid_base_url","message":"Use a base URL on the environment's host or one of its allowed domains, not a private address. A wildcard over shared hosting, such as *.vercel.app, doesn't count: allow the exact host.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unprocessable":{"summary":"unprocessable","value":{"error":{"code":"unprocessable","message":"The request can't be processed.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"idempotency_mismatch":{"summary":"idempotency_mismatch","value":{"error":{"code":"idempotency_mismatch","message":"This Idempotency-Key was used with a different request.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/runs/{runId}/tests/{testRunId}/report":{"get":{"operationId":"getTestReport","summary":"Get a test's failure report (pending until it is written; a passed or still-running test has none: 404 not_found)","description":"The failure report of a test that failed or errored: a structured report and the same as Markdown. While it is being written status is pending; poll again shortly. A passed test, or one still running, has no report: 404 not_found.\n\nNeeds an API key with the read scope.","tags":["Results"],"security":[{"apiKey":[]}],"x-outqube-scope":"read","parameters":[{"name":"runId","in":"path","required":true,"description":"The run's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"},{"name":"testRunId","in":"path","required":true,"description":"The test_run_id of one of the run's tests.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"c2e5a7b9-1d3f-4a68-b0c4-8e9d2f1a3b57"}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestReport"},"examples":{"default":{"summary":"Example response","value":{"status":"ready","report":{"title":"Sign-in fails with a valid account","summary":"After selecting Sign in the page stays on the form and shows an error."},"markdown":"## Sign-in fails with a valid account\n\nAfter selecting **Sign in** the page stays on the form and shows \"Something went wrong\"."}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/runs/{runId}/tests/{testRunId}/steps":{"get":{"operationId":"listSteps","summary":"List a test's steps in order","description":"The steps of one test, in order. Use getStep for the reasoning, console and network entries and the screenshot of one.\n\nNeeds an API key with the read scope.","tags":["Results"],"security":[{"apiKey":[]}],"x-outqube-scope":"read","parameters":[{"name":"runId","in":"path","required":true,"description":"The run's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"},{"name":"testRunId","in":"path","required":true,"description":"The test_run_id of one of the run's tests.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"c2e5a7b9-1d3f-4a68-b0c4-8e9d2f1a3b57"},{"name":"limit","in":"query","required":false,"description":"Items per page, 1 to 100.","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"The next_cursor of the previous page. Opaque: pass it back as given.","schema":{"type":"string","minLength":1,"maxLength":200}}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/StepSummary"},"description":"The items of this page."},"next_cursor":{"description":"Pass as cursor to get the next page; null on the last page.","type":["string","null"]}},"required":["data","next_cursor"],"additionalProperties":false},"examples":{"default":{"summary":"Example response","value":{"data":[{"n":3,"action":"click","summary":"Select Sign in","url":"https://staging.acme.test/sign-in","outcome":"error","at":"2026-10-30T12:01:12.000Z"}],"next_cursor":null}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/runs/{runId}/tests/{testRunId}/steps/{n}":{"get":{"operationId":"getStep","summary":"Get one step with the console and network entries of that step","description":"Needs an API key with the read scope.","tags":["Results"],"security":[{"apiKey":[]}],"x-outqube-scope":"read","parameters":[{"name":"runId","in":"path","required":true,"description":"The run's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"},{"name":"testRunId","in":"path","required":true,"description":"The test_run_id of one of the run's tests.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"c2e5a7b9-1d3f-4a68-b0c4-8e9d2f1a3b57"},{"name":"n","in":"path","required":true,"description":"The step number, from 1.","schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"example":3}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StepDetail"},"examples":{"default":{"summary":"Example response","value":{"n":3,"action":"click","summary":"Select Sign in","url":"https://staging.acme.test/sign-in","outcome":"error","at":"2026-10-30T12:01:12.000Z","reasoning":"The sign-in button is visible and enabled, so select it to submit the form.","console":[{"step":3,"kind":"console","level":"error","message":"Uncaught TypeError: Cannot read properties of undefined (reading 'id')","url":null,"method":null,"status":null,"at":"2026-10-30T12:01:12.450Z"}],"network":[{"step":3,"kind":"network","level":null,"message":"POST /api/session failed","url":"https://staging.acme.test/api/session","method":"POST","status":500,"at":"2026-10-30T12:01:12.400Z"}],"screenshot_artifact_id":"art_c2e5a7b91d3f4a68b0c48e9d2f1a3b57"}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/runs/{runId}/tests/{testRunId}/logs":{"get":{"operationId":"listLogs","summary":"List a test's console or network entries","description":"One kind of entry per call: console or network.\n\nNeeds an API key with the read scope.","tags":["Results"],"security":[{"apiKey":[]}],"x-outqube-scope":"read","parameters":[{"name":"runId","in":"path","required":true,"description":"The run's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"},{"name":"testRunId","in":"path","required":true,"description":"The test_run_id of one of the run's tests.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"c2e5a7b9-1d3f-4a68-b0c4-8e9d2f1a3b57"},{"name":"limit","in":"query","required":false,"description":"Items per page, 1 to 100.","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"The next_cursor of the previous page. Opaque: pass it back as given.","schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"kind","in":"query","required":true,"description":"Which entries to list: console or network.","schema":{"type":"string","enum":["console","network"]}},{"name":"errors_only","in":"query","required":false,"description":"Only errors; default false.","schema":{"type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"},"description":"The items of this page."},"next_cursor":{"description":"Pass as cursor to get the next page; null on the last page.","type":["string","null"]}},"required":["data","next_cursor"],"additionalProperties":false},"examples":{"default":{"summary":"Example response","value":{"data":[{"step":3,"kind":"network","level":null,"message":"POST /api/session failed","url":"https://staging.acme.test/api/session","method":"POST","status":500,"at":"2026-10-30T12:01:12.400Z"},{"step":3,"kind":"console","level":"error","message":"Uncaught TypeError: Cannot read properties of undefined (reading 'id')","url":null,"method":null,"status":null,"at":"2026-10-30T12:01:12.450Z"}],"next_cursor":null}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/artifacts/{artifactId}":{"get":{"operationId":"getArtifact","summary":"Redirect to a signed URL for a screenshot or video, valid for up to 15 minutes","description":"Answers 302 with the signed URL in the Location header and no body. Follow the redirect to download the file; do not store the URL, it expires. 404 not_found when the artifact is not in this app or its evidence has been removed (retention).\n\nNeeds an API key with the read scope.","tags":["Artifacts"],"security":[{"apiKey":[]}],"x-outqube-scope":"read","parameters":[{"name":"artifactId","in":"path","required":true,"description":"An artifact id, such as a step's screenshot_artifact_id.","schema":{"type":"string","pattern":"^art_[A-Za-z0-9_-]{4,200}$"},"example":"art_c2e5a7b91d3f4a68b0c48e9d2f1a3b57"}],"responses":{"302":{"description":"Redirect to the signed URL. The response has no body.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"location":{"description":"The signed URL of the file. It expires within 15 minutes.","schema":{"type":"string","format":"uri"}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/webhooks":{"get":{"operationId":"listWebhooks","summary":"List the app's event webhooks, newest first","description":"Pages of the app's endpoints, newest first. The signing secret is not included.\n\nNeeds an API key with the manage scope.","tags":["Webhooks"],"security":[{"apiKey":[]}],"x-outqube-scope":"manage","parameters":[{"name":"limit","in":"query","required":false,"description":"Items per page, 1 to 100.","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"The next_cursor of the previous page. Opaque: pass it back as given.","schema":{"type":"string","minLength":1,"maxLength":200}}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"},"description":"The items of this page."},"next_cursor":{"description":"Pass as cursor to get the next page; null on the last page.","type":["string","null"]}},"required":["data","next_cursor"],"additionalProperties":false},"examples":{"default":{"summary":"Example response","value":{"data":[{"id":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","url":"https://ci.acme.test/hooks/outqube","events":["run.completed","run.cancelled"],"description":"Post results to CI","enabled":true,"disabled_reason":null,"disabled_at":null,"consecutive_failures":0,"secret_hint":"k3Fz","previous_secret_expires_at":null,"created_at":"2026-10-25T10:00:00.000Z","updated_at":"2026-10-25T10:00:00.000Z"}],"next_cursor":null}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}},"post":{"operationId":"createWebhook","summary":"Create an event webhook (10 per app at most)","description":"Adds an HTTPS endpoint. The URL must be https, on a public host and a standard port (400 invalid_request otherwise). The response includes the signing secret; this and rotateWebhookSecret are the only calls that return it, so store it now. 409 conflict when the app already has 10 endpoints. See Webhook events for what is sent and how to verify it.\n\nNeeds an API key with the manage scope.","tags":["Webhooks"],"security":[{"apiKey":[]}],"x-outqube-scope":"manage","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","minLength":1,"maxLength":2048,"description":"An https:// URL on a public host, standard port."},"events":{"anyOf":[{"type":"array","prefixItems":[{"type":"string","const":"*"}],"items":false,"minItems":1,"maxItems":1},{"minItems":1,"maxItems":5,"type":"array","items":{"type":"string","enum":["run.queued","run.started","run.needs_attention","run.completed","run.cancelled"]}}],"description":"Event types to send, or [\"*\"] for all of them."},"description":{"default":"","description":"Your note on what the endpoint is for, up to 200 characters.","type":"string","maxLength":200},"enabled":{"default":true,"description":"Start sending straight away; default true.","type":"boolean"}},"required":["url"]},"examples":{"default":{"summary":"Example request","value":{"url":"https://ci.acme.test/hooks/outqube","events":["run.completed","run.cancelled"],"description":"Post results to CI"}}}}}},"responses":{"201":{"description":"Created","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookWithSecret"},"examples":{"default":{"summary":"Example response","value":{"id":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","url":"https://ci.acme.test/hooks/outqube","events":["run.completed","run.cancelled"],"description":"Post results to CI","enabled":true,"disabled_reason":null,"disabled_at":null,"consecutive_failures":0,"secret_hint":"k3Fz","previous_secret_expires_at":null,"created_at":"2026-10-25T10:00:00.000Z","updated_at":"2026-10-25T10:00:00.000Z","secret":"whsec_c2FtcGxlLXdlYmhvb2sta2V5LW5vdC1hLXNlY3JldCE="}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"409":{"description":"Conflict with the current state.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"conflict":{"summary":"conflict","value":{"error":{"code":"conflict","message":"That conflicts with the current state.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/webhooks/{webhookId}":{"get":{"operationId":"getWebhook","summary":"Get one event webhook","description":"One endpoint, without its signing secret.\n\nNeeds an API key with the manage scope.","tags":["Webhooks"],"security":[{"apiKey":[]}],"x-outqube-scope":"manage","parameters":[{"name":"webhookId","in":"path","required":true,"description":"The endpoint's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46"}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"},"examples":{"default":{"summary":"Example response","value":{"id":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","url":"https://ci.acme.test/hooks/outqube","events":["run.completed","run.cancelled"],"description":"Post results to CI","enabled":true,"disabled_reason":null,"disabled_at":null,"consecutive_failures":0,"secret_hint":"k3Fz","previous_secret_expires_at":null,"created_at":"2026-10-25T10:00:00.000Z","updated_at":"2026-10-25T10:00:00.000Z"}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}},"patch":{"operationId":"updateWebhook","summary":"Change an event webhook's URL, events, description or on/off","description":"Changes the fields you send. A new URL is checked as on create. Turning an endpoint on again clears its disabled_reason and failure count.\n\nNeeds an API key with the manage scope.","tags":["Webhooks"],"security":[{"apiKey":[]}],"x-outqube-scope":"manage","parameters":[{"name":"webhookId","in":"path","required":true,"description":"The endpoint's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","minLength":1,"maxLength":2048,"description":"An https:// URL on a public host, standard port."},"events":{"anyOf":[{"type":"array","prefixItems":[{"type":"string","const":"*"}],"items":false,"minItems":1,"maxItems":1},{"minItems":1,"maxItems":5,"type":"array","items":{"type":"string","enum":["run.queued","run.started","run.needs_attention","run.completed","run.cancelled"]}}],"description":"Event types to send, or [\"*\"] for all of them."},"description":{"type":"string","maxLength":200,"description":"Your note on what the endpoint is for, up to 200 characters."},"enabled":{"type":"boolean","description":"Turning it on again clears a disabled_reason and the failure count."}},"description":"The fields to change. Send at least one; anything left out stays as it is."},"examples":{"default":{"summary":"Example request","value":{"events":["*"],"enabled":true}}}}}},"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"},"examples":{"default":{"summary":"Example response","value":{"id":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","url":"https://ci.acme.test/hooks/outqube","events":["run.completed","run.cancelled"],"description":"Post results to CI","enabled":true,"disabled_reason":null,"disabled_at":null,"consecutive_failures":0,"secret_hint":"k3Fz","previous_secret_expires_at":null,"created_at":"2026-10-25T10:00:00.000Z","updated_at":"2026-10-25T10:00:00.000Z"}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}},"delete":{"operationId":"deleteWebhook","summary":"Delete an event webhook and its delivery log","description":"Removes the endpoint for good, with its delivery log.\n\nNeeds an API key with the manage scope.","tags":["Webhooks"],"security":[{"apiKey":[]}],"x-outqube-scope":"manage","parameters":[{"name":"webhookId","in":"path","required":true,"description":"The endpoint's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46"}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The deleted endpoint's id."},"deleted":{"type":"boolean","const":true,"description":"Always true."}},"required":["id","deleted"],"additionalProperties":false},"examples":{"default":{"summary":"Example response","value":{"id":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46","deleted":true}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/webhooks/{webhookId}/rotate-secret":{"post":{"operationId":"rotateWebhookSecret","summary":"Make a new signing secret; the old one keeps signing for 24 hours","description":"Returns the new secret. For 24 hours each message carries two signatures, one per secret, so a receiver can switch without missing a message.\n\nNeeds an API key with the manage scope.","tags":["Webhooks"],"security":[{"apiKey":[]}],"x-outqube-scope":"manage","parameters":[{"name":"webhookId","in":"path","required":true,"description":"The endpoint's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46"}],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookWithSecret"},"examples":{"default":{"summary":"Example response","value":{"id":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","url":"https://ci.acme.test/hooks/outqube","events":["run.completed","run.cancelled"],"description":"Post results to CI","enabled":true,"disabled_reason":null,"disabled_at":null,"consecutive_failures":0,"secret_hint":"k3Fz","previous_secret_expires_at":"2026-10-31T10:00:00.000Z","created_at":"2026-10-25T10:00:00.000Z","updated_at":"2026-10-25T10:00:00.000Z","secret":"whsec_c2FtcGxlLXdlYmhvb2sta2V5LW5vdC1hLXNlY3JldCE="}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/webhooks/{webhookId}/test":{"post":{"operationId":"testWebhook","summary":"Send a ping test event to the endpoint","description":"Queues one ping message to the endpoint, signed like any event, and answers 202 at once; the delivery happens shortly after. A ping is sent once and not retried. Calling this again for the same endpoint within 10 seconds is a 429 rate_limited.\n\nNeeds an API key with the manage scope.","tags":["Webhooks"],"security":[{"apiKey":[]}],"x-outqube-scope":"manage","parameters":[{"name":"webhookId","in":"path","required":true,"description":"The endpoint's id.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"example":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46"}],"responses":{"202":{"description":"Accepted","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"type":"object","properties":{"delivery_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The delivery's id in the endpoint's delivery log."},"message_id":{"type":"string","description":"The webhook-id header of the test message: ping_ and a UUID."}},"required":["delivery_id","message_id"],"additionalProperties":false},"examples":{"default":{"summary":"Example response","value":{"delivery_id":"62a8c4e0-f1b3-4d57-8e9a-0b2d4f6a8c13","message_id":"ping_0f6c2e8a-4b1d-4a73-9c5e-7d3b1a9f2e68"}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/hooks":{"get":{"operationId":"getHooks","summary":"Get the app's setup and teardown hooks","description":"The app's hooks, without the signing secret. 404 not_found until the hooks are first saved with putHooks.\n\nNeeds an API key with the manage scope.","tags":["Hooks"],"security":[{"apiKey":[]}],"x-outqube-scope":"manage","parameters":[],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Hooks"},"examples":{"default":{"summary":"Example response","value":{"app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","setup_url":"https://staging.acme.test/outqube/setup","teardown_url":"https://staging.acme.test/outqube/teardown","timeout_seconds":30,"enabled":true,"secret_hint":"q7Wd","previous_secret_expires_at":null,"created_at":"2026-10-25T10:05:00.000Z","updated_at":"2026-10-25T10:05:00.000Z"}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}},"put":{"operationId":"putHooks","summary":"Set the app's setup and teardown hooks","description":"Replaces the app's hooks. The first save makes the signing secret and returns it; later saves keep it. Both URLs are checked: https, a public host, standard port (400 invalid_request otherwise).\n\nNeeds an API key with the manage scope.","tags":["Hooks"],"security":[{"apiKey":[]}],"x-outqube-scope":"manage","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"setup_url":{"default":null,"description":"Called before a run's first test: an https:// URL, or null for none.","anyOf":[{"type":"string","maxLength":2048},{"type":"null"}]},"teardown_url":{"default":null,"description":"Called after a run's last test: an https:// URL, or null for none.","anyOf":[{"type":"string","maxLength":2048},{"type":"null"}]},"timeout_seconds":{"default":30,"description":"Seconds to wait for an answer, 1 to 120; default 30.","type":"integer","minimum":1,"maximum":120},"enabled":{"default":true,"description":"Whether Outqube calls the hooks; default true.","type":"boolean"}},"description":"Replaces the app's hooks. Anything left out gets its default."},"examples":{"default":{"summary":"Example request","value":{"setup_url":"https://staging.acme.test/outqube/setup","teardown_url":"https://staging.acme.test/outqube/teardown","timeout_seconds":30,"enabled":true}}}}}},"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HooksSaved"},"examples":{"default":{"summary":"Example response","value":{"app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","setup_url":"https://staging.acme.test/outqube/setup","teardown_url":"https://staging.acme.test/outqube/teardown","timeout_seconds":30,"enabled":true,"secret_hint":"q7Wd","previous_secret_expires_at":null,"created_at":"2026-10-25T10:05:00.000Z","updated_at":"2026-10-25T10:05:00.000Z","secret":"whsec_c2FtcGxlLWhvb2tzLWtleS1ub3QtYS1zZWNyZXQhISE="}}}}}},"400":{"description":"Invalid request: a query parameter, header or the body is not valid.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_request":{"summary":"invalid_request","value":{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/hooks/rotate-secret":{"post":{"operationId":"rotateHooksSecret","summary":"Make a new hook signing secret; the old one keeps signing for 24 hours","description":"Returns the new secret. For 24 hours each call carries two signatures, one per secret. 404 not_found until the hooks are first saved.\n\nNeeds an API key with the manage scope.","tags":["Hooks"],"security":[{"apiKey":[]}],"x-outqube-scope":"manage","parameters":[],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HooksWithSecret"},"examples":{"default":{"summary":"Example response","value":{"app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","setup_url":"https://staging.acme.test/outqube/setup","teardown_url":"https://staging.acme.test/outqube/teardown","timeout_seconds":30,"enabled":true,"secret_hint":"q7Wd","previous_secret_expires_at":"2026-10-31T10:05:00.000Z","created_at":"2026-10-25T10:05:00.000Z","updated_at":"2026-10-25T10:05:00.000Z","secret":"whsec_c2FtcGxlLWhvb2tzLWtleS1ub3QtYS1zZWNyZXQhISE="}}}}}},"401":{"description":"Missing, malformed, unknown, revoked or expired API key, or the organisation can't use the API right now.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"www-authenticate":{"description":"Always Bearer.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"unauthorized","value":{"error":{"code":"unauthorized","message":"Missing or invalid API key.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"403":{"description":"The key's scope is too low for this endpoint.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This API key's scope doesn't allow that.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"404":{"description":"Not found in this app.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"Not found.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"429":{"description":"Rate limited. Each key may make 120 requests a minute by default, and each IP address has a looser limit. Wait for the Retry-After seconds.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again after the Retry-After seconds.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}},"/openapi.json":{"get":{"operationId":"getOpenApi","summary":"This OpenAPI 3.1 document","description":"The OpenAPI 3.1 document of this API, as JSON. Public: it needs no API key. It can be cached for 5 minutes.","tags":["Meta"],"security":[],"parameters":[],"responses":{"200":{"description":"OK","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The OpenAPI 3.1 document."},"examples":{"default":{"summary":"Example response","value":{"openapi":"3.1.0","info":{"title":"Outqube API","version":"1"},"servers":[{"url":"https://app.outqube.com/api/v1"}]}}}}}},"default":{"description":"An unexpected error: 500 internal, or 503 unavailable when the service can't be reached. Try again.","headers":{"request-id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"internal","value":{"error":{"code":"internal","message":"Something went wrong. Try again.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}},"unavailable":{"summary":"unavailable","value":{"error":{"code":"unavailable","message":"The service is unavailable. Try again shortly.","request_id":"8e5b1c4a-2f7d-4a93-b6e0-5d1c3a9f7e24"}}}}}}}}}}},"webhooks":{"run.queued":{"post":{"operationId":"webhookRunQueued","summary":"Run event: run.queued","description":"The run was created with its test runs. Sent once per run.","tags":["Webhook events"],"security":[],"parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The message id. The same on every retry of the message, so use it to drop duplicates.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed.","schema":{"type":"string","pattern":"^[0-9]+$"}},{"name":"webhook-signature","in":"header","required":true,"description":"v1,<base64 signature> for each active secret, separated by a space. Two during the 24 hours after a secret rotation; accept the message if either matches.","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"The message body. Verify the signature against the raw bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunEvent"},"examples":{"default":{"summary":"Example message","value":{"type":"run.queued","timestamp":"2026-10-30T12:00:00.123Z","data":{"run":{"id":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","status":"queued","outcome":null,"source":"github","commit_sha":"4f1c2a9","branch":"feature/login","pr_number":42,"repo":"acme/web","counts":{"total":12,"passed":0,"failed":0,"error":0},"url":"https://app.outqube.com/acme/runs?execution=3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","api_url":"https://app.outqube.com/api/v1/runs/3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"}}}}}}}},"responses":{"2XX":{"description":"Delivered. Any 2xx status, within 10 seconds. The response body is ignored."},"default":{"description":"Not delivered: any other status, no answer in time, a connection error or a redirect. The message is sent again on the retry schedule (see the Webhook events tag)."}}}},"run.started":{"post":{"operationId":"webhookRunStarted","summary":"Run event: run.started","description":"The first test run of the run started. Sent once per run.","tags":["Webhook events"],"security":[],"parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The message id. The same on every retry of the message, so use it to drop duplicates.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed.","schema":{"type":"string","pattern":"^[0-9]+$"}},{"name":"webhook-signature","in":"header","required":true,"description":"v1,<base64 signature> for each active secret, separated by a space. Two during the 24 hours after a secret rotation; accept the message if either matches.","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"The message body. Verify the signature against the raw bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunEvent"},"examples":{"default":{"summary":"Example message","value":{"type":"run.started","timestamp":"2026-10-30T12:00:04.456Z","data":{"run":{"id":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","status":"running","outcome":null,"source":"github","commit_sha":"4f1c2a9","branch":"feature/login","pr_number":42,"repo":"acme/web","counts":{"total":12,"passed":0,"failed":0,"error":0},"url":"https://app.outqube.com/acme/runs?execution=3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","api_url":"https://app.outqube.com/api/v1/runs/3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"}}}}}}}},"responses":{"2XX":{"description":"Delivered. Any 2xx status, within 10 seconds. The response body is ignored."},"default":{"description":"Not delivered: any other status, no answer in time, a connection error or a redirect. The message is sent again on the retry schedule (see the Webhook events tag)."}}}},"run.needs_attention":{"post":{"operationId":"webhookRunNeedsAttention","summary":"Run event: run.needs_attention","description":"A test run asked for help and is waiting for an answer (status waiting_for_input). Sent each time one starts waiting; data.test_run says which. Runs started from the API, the CLI, GitHub or a schedule never wait: a question ends the test run as blocked.","tags":["Webhook events"],"security":[],"parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The message id. The same on every retry of the message, so use it to drop duplicates.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed.","schema":{"type":"string","pattern":"^[0-9]+$"}},{"name":"webhook-signature","in":"header","required":true,"description":"v1,<base64 signature> for each active secret, separated by a space. Two during the 24 hours after a secret rotation; accept the message if either matches.","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"The message body. Verify the signature against the raw bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunEvent"},"examples":{"default":{"summary":"Example message","value":{"type":"run.needs_attention","timestamp":"2026-10-30T12:01:30.789Z","data":{"run":{"id":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","status":"running","outcome":null,"source":"github","commit_sha":"4f1c2a9","branch":"feature/login","pr_number":42,"repo":"acme/web","counts":{"total":12,"passed":4,"failed":0,"error":0},"url":"https://app.outqube.com/acme/runs?execution=3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","api_url":"https://app.outqube.com/api/v1/runs/3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"},"test_run":{"id":"c2e5a7b9-1d3f-4a68-b0c4-8e9d2f1a3b57","test_id":"5d7e9a1c-3b2f-4c84-a6e0-1f8b3d5c7a92","status":"waiting_for_input"}}}}}}}},"responses":{"2XX":{"description":"Delivered. Any 2xx status, within 10 seconds. The response body is ignored."},"default":{"description":"Not delivered: any other status, no answer in time, a connection error or a redirect. The message is sent again on the retry schedule (see the Webhook events tag)."}}}},"run.completed":{"post":{"operationId":"webhookRunCompleted","summary":"Run event: run.completed","description":"Every test run ended and none was stopped. data.run.outcome is passed, failed or error: failed if any test failed, else error if any errored, was blocked or inconclusive, else passed. Sent once per run.","tags":["Webhook events"],"security":[],"parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The message id. The same on every retry of the message, so use it to drop duplicates.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed.","schema":{"type":"string","pattern":"^[0-9]+$"}},{"name":"webhook-signature","in":"header","required":true,"description":"v1,<base64 signature> for each active secret, separated by a space. Two during the 24 hours after a secret rotation; accept the message if either matches.","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"The message body. Verify the signature against the raw bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunEvent"},"examples":{"default":{"summary":"Example message","value":{"type":"run.completed","timestamp":"2026-10-30T12:03:41.001Z","data":{"run":{"id":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","status":"completed","outcome":"failed","source":"github","commit_sha":"4f1c2a9","branch":"feature/login","pr_number":42,"repo":"acme/web","counts":{"total":12,"passed":10,"failed":2,"error":0},"url":"https://app.outqube.com/acme/runs?execution=3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","api_url":"https://app.outqube.com/api/v1/runs/3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"}}}}}}}},"responses":{"2XX":{"description":"Delivered. Any 2xx status, within 10 seconds. The response body is ignored."},"default":{"description":"Not delivered: any other status, no answer in time, a connection error or a redirect. The message is sent again on the retry schedule (see the Webhook events tag)."}}}},"run.cancelled":{"post":{"operationId":"webhookRunCancelled","summary":"Run event: run.cancelled","description":"Every test run ended and at least one was stopped, by a person or because it ran too long. There is no outcome. Sent once per run.","tags":["Webhook events"],"security":[],"parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The message id. The same on every retry of the message, so use it to drop duplicates.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed.","schema":{"type":"string","pattern":"^[0-9]+$"}},{"name":"webhook-signature","in":"header","required":true,"description":"v1,<base64 signature> for each active secret, separated by a space. Two during the 24 hours after a secret rotation; accept the message if either matches.","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"The message body. Verify the signature against the raw bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunEvent"},"examples":{"default":{"summary":"Example message","value":{"type":"run.cancelled","timestamp":"2026-10-30T12:01:10.250Z","data":{"run":{"id":"3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","status":"cancelled","outcome":null,"source":"github","commit_sha":"4f1c2a9","branch":"feature/login","pr_number":42,"repo":"acme/web","counts":{"total":12,"passed":5,"failed":0,"error":0},"url":"https://app.outqube.com/acme/runs?execution=3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61","api_url":"https://app.outqube.com/api/v1/runs/3f8a6c1e-5b2d-4e7a-8c19-0d4b7e2a9f61"}}}}}}}},"responses":{"2XX":{"description":"Delivered. Any 2xx status, within 10 seconds. The response body is ignored."},"default":{"description":"Not delivered: any other status, no answer in time, a connection error or a redirect. The message is sent again on the retry schedule (see the Webhook events tag)."}}}},"ping":{"post":{"operationId":"webhookPing","summary":"Test message: ping","description":"Sent once when you call testWebhook, to check that your endpoint receives and verifies messages. It is not retried.","tags":["Webhook events"],"security":[],"parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The message id. The same on every retry of the message, so use it to drop duplicates.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"Unix seconds when this attempt was signed.","schema":{"type":"string","pattern":"^[0-9]+$"}},{"name":"webhook-signature","in":"header","required":true,"description":"v1,<base64 signature> for each active secret, separated by a space. Two during the 24 hours after a secret rotation; accept the message if either matches.","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"The message body. Verify the signature against the raw bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PingEvent"},"examples":{"default":{"summary":"Example message","value":{"type":"ping","timestamp":"2026-10-30T12:00:00.000Z","data":{"app_id":"9b1d2f4e-7a3c-4c58-9e0b-6f3a1d2c4b5a","webhook_id":"d8f0b2a4-6c1e-4d73-9a5b-1c3e5a7b9d46"}}}}}}},"responses":{"2XX":{"description":"Delivered. Any 2xx status, within 10 seconds. The response body is ignored."},"default":{"description":"Not delivered: any other status, no answer in time, a connection error or a redirect. The message is sent again on the retry schedule (see the Webhook events tag)."}}}}}}