{"openapi":"3.1.0","info":{"title":"RowAttest Run API","version":"1.0.0","description":"Start RowAttest runs on projects you have already registered in the dashboard, follow them, and read their results. Keys are created on the API keys page. Runs started with a key count against the account's monthly allowance, the same as runs started in the dashboard. Nothing can be created or changed through the API except starting runs, and nothing is paid for through it."},"servers":[{"url":"https://app.rowattest.com"}],"security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"A RowAttest API key: \"Authorization: Bearer rak_…\"."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["unauthorized","not_found","run_in_progress","project_not_ready","project_not_testable","run_not_finished","rate_limited","run_limit_reached","method_not_allowed","invalid_request","idempotency_key_reused","report_not_ready","report_unavailable","unavailable","consent_required"]},"message":{"type":"string"},"allowance":{"type":"integer","description":"The free tier's monthly run allowance."},"used":{"type":"integer","description":"Runs counted this calendar month."},"resets_at":{"type":"string","format":"date-time","description":"When the allowance resets: the start of next month, UTC."},"url":{"type":"string","format":"uri"},"run_id":{"type":["string","null"],"format":"uuid"},"reason":{"type":"string","enum":["reconnect_required","database_password_required","fit_check_pending","fit_check_needed"]},"status":{"type":"string","enum":["queued","running","complete","failed"]},"poll_after_seconds":{"type":"integer"}}}}},"Project":{"type":"object","required":["project_id","name","project_ref","ready","reason","fit","access_model","last_run"],"properties":{"project_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"project_ref":{"type":["string","null"]},"ready":{"type":"boolean","description":"Whether a run can start now, as on the dashboard: the project can connect and its latest complete fit check found at least one table RowAttest can test."},"reason":{"type":["string","null"],"enum":["reconnect_required","database_password_required","fit_check_pending","fit_check_needed","no_testable_tables",null],"description":"Why ready is false: the connection first, then the fit check (pending: a check is running; needed: none has completed; no_testable_tables: the latest complete check found none)."},"fit":{"type":"object","description":"The project's fit check: which tables RowAttest can test. state is the latest check's; testable, total and checked_at come from the latest complete check (null when none has completed).","required":["state","testable","total","checked_at"],"properties":{"state":{"type":"string","enum":["none","checking","complete","failed"]},"testable":{"type":["integer","null"]},"total":{"type":["integer","null"]},"checked_at":{"type":["string","null"],"format":"date-time"}}},"access_model":{"type":"object","description":"The project's access model, from its latest saved version. state detected: no version has been saved. confirmed: a version has been saved and no completed check has found it out of date. needs_attention: the latest check of the saved version found that it no longer verifies or that the schema changed since it was saved. sha256 is the version's SHA-256 once a check has evaluated it; saved_at is when the version was saved.","required":["version","state","sha256","saved_at"],"properties":{"version":{"type":["integer","null"]},"state":{"type":"string","enum":["detected","confirmed","needs_attention"]},"sha256":{"type":["string","null"]},"saved_at":{"type":["string","null"],"format":"date-time"}}},"last_run":{"oneOf":[{"type":"null"},{"type":"object","required":["run_id","status","finished_at"],"properties":{"run_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","running","complete","failed"]},"finished_at":{"type":["string","null"],"format":"date-time"}}}]}}},"RunStarted":{"type":"object","required":["run_id","status","created_at","poll_after_seconds"],"properties":{"run_id":{"type":"string","format":"uuid"},"status":{"type":"string","const":"queued"},"created_at":{"type":"string","format":"date-time"},"poll_after_seconds":{"type":"integer"}}},"Run":{"type":"object","required":["run_id","project_id","status","stage","created_at","started_at","finished_at","report_id","verify_url","poll_after_seconds","access_model_sha256"],"properties":{"run_id":{"type":"string","format":"uuid"},"project_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","running","complete","failed"]},"stage":{"type":["integer","null"],"minimum":0,"maximum":7},"created_at":{"type":"string","format":"date-time"},"started_at":{"type":["string","null"],"format":"date-time"},"finished_at":{"type":["string","null"],"format":"date-time"},"report_id":{"type":["string","null"]},"verify_url":{"type":["string","null"],"format":"uri"},"poll_after_seconds":{"type":["integer","null"]},"access_model_sha256":{"type":["string","null"],"description":"SHA-256 of the access model this run used, as the run's report states it. A signed report for the run embeds the same model and states the same value."}}},"Findings":{"type":"object","description":"A complete run with a signed report answers from that report: verification is signed, with its report_id and verify_url. For a complete run with no signed report, while one can still be bought on the run page, the answer is the run's unverified result instead, as the run page shows it: verification is unverified, report_id and verify_url are null, url is the run page, each finding has only its verdict, surface, operation and principal, findings_lines carries the run page's Findings section line by line, counts.not_evaluated is null (the coverage lines state that number), and coverage and access_model come from the run's recorded result. The fields summary, clean_report_blocked, allowed_by_model, flags and blocking_findings come only with a signed report.","required":["run_id","verification","report_id","verify_url","counts","findings"],"properties":{"run_id":{"type":"string","format":"uuid"},"verification":{"type":"string","enum":["signed","unverified"]},"report_id":{"type":["string","null"]},"verify_url":{"type":["string","null"],"format":"uri"},"url":{"type":"string","format":"uri","description":"Unverified only: the run page, where a signed report for the run can be bought."},"counts":{"type":"object","required":["pass","fail","provisional","not_evaluated","error","allowed_by_model"],"properties":{"pass":{"type":"integer"},"fail":{"type":"integer","description":"leak + disagreement + model_divergence"},"provisional":{"type":"integer","description":"layer_a_only + layer_b_corroborated"},"not_evaluated":{"type":["integer","null"],"description":"null when verification is unverified"},"error":{"type":"integer"},"allowed_by_model":{"type":"integer","description":"The number of cells in allowed_by_model."}}},"summary":{"type":"object","description":"The report's differential summary, verbatim."},"clean_report_blocked":{},"findings":{"type":"array","description":"Every cell whose verdict is not PASS.","items":{"type":"object","properties":{"cell_id":{"type":"string"},"verdict":{"type":"string"},"surface":{"type":"string","description":"The surface the cell ran on: table:<schema>.<name> in a report 2.0, table:<name> in a report 1.0, or bucket:<name>."},"operation":{"type":"string"},"boundary":{"type":"string"},"identity":{"type":"object","properties":{"label":{"type":"string"},"principal":{"type":"string"},"role":{"type":"string"}}},"principal":{"type":"string","description":"Unverified only: the test identity, as the run page names it."},"layer_a":{"type":"string"},"layer_b":{"type":["string","null"]},"note":{"type":"string"},"identifiers":{"type":"array","items":{"type":"object","properties":{"scheme":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}}}}}}},"findings_lines":{"type":"array","items":{"type":"string"},"description":"Unverified only: the run page's Findings section, line by line"},"coverage":{"type":"array","items":{"type":"string"},"description":"Unverified only: the coverage lines, as the run page lists them."},"access_model":{"type":["object","null"],"description":"Unverified only: the access model summary the run kept (null for a run without one).","properties":{"sha256":{"type":"string"},"version":{"type":["integer","null"]},"basis_counts":{"type":"object","properties":{"detected":{"type":"integer"},"confirmed":{"type":"integer"},"declared":{"type":"integer"}}},"tables":{"type":"object","properties":{"covered":{"type":"integer"},"declared_public":{"type":"integer"},"not_evaluated":{"type":"integer"},"total":{"type":"integer"}}}}},"allowed_by_model":{"type":"array","description":"The report's cells whose access the access model allows, for example reads of a table detected as public. They are not PASS, so they are also in findings. Empty for a report without an access model.","items":{"type":"object","properties":{"cell_id":{"type":"string"},"surface":{"type":"string","description":"The surface the cell ran on: table:<schema>.<name> in a report 2.0, table:<name> in a report 1.0, or bucket:<name>."},"operation":{"type":"string"},"identity":{"type":"object","properties":{"label":{"type":"string"},"principal":{"type":"string"},"role":{"type":"string"}}},"layer_a":{"type":"string"},"layer_b":{"type":["string","null"]},"note":{"type":"string"}}}},"flags":{"type":"array","description":"Schema-level flags (classes C1–C3).","items":{"type":"object"}},"blocking_findings":{"type":"array","description":"Verbatim from the report.","items":{"type":"object"}}}}}},"paths":{"/api/v1/projects":{"get":{"summary":"List the projects registered on the key's account","responses":{"200":{"description":"The projects","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"examples":{"projects":{"summary":"One project ready to run, one waiting for its database password","value":[{"project_id":"5eef4c90-9cac-46e3-9c9a-5b6d722b3952","name":"Staging","project_ref":"abcdefghijklmnopqrst","ready":true,"reason":null,"fit":{"state":"complete","testable":7,"total":8,"checked_at":"2026-10-07T20:19:03.965+00:00"},"access_model":{"version":null,"state":"detected","sha256":null,"saved_at":null},"last_run":{"run_id":"2c949779-85a5-4fea-929c-4ebf99db6eaf","status":"complete","finished_at":"2026-10-08T04:40:16.908+00:00"}},{"project_id":"9a0d7c3b-1e2f-4a5b-8c6d-7e8f9a0b1c2d","name":"Preview","project_ref":"tsrqponmlkjihgfedcba","ready":false,"reason":"database_password_required","fit":{"state":"none","testable":null,"total":null,"checked_at":null},"access_model":{"version":null,"state":"detected","sha256":null,"saved_at":null},"last_run":null}]}}}}},"401":{"description":"Missing, malformed or unknown key","headers":{"WWW-Authenticate":{"description":"The scheme to use","schema":{"type":"string"},"example":"Bearer"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"Missing, malformed or unknown key","value":{"error":{"code":"unauthorized","message":"This request needs a valid RowAttest API key in the Authorization header."}}}}}},"x-error-codes":["unauthorized"]},"403":{"description":"The key's account has not accepted the Terms of Service: sign in at app.rowattest.com to accept them","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"consent_required":{"summary":"The key's account has not accepted the Terms of Service","value":{"error":{"code":"consent_required","message":"This account has not accepted the RowAttest Terms of Service. Sign in at app.rowattest.com to accept them, then try again."}}}}}},"x-error-codes":["consent_required"]},"405":{"description":"The endpoint does not accept that method","headers":{"Allow":{"description":"The method this endpoint accepts","schema":{"type":"string"},"example":"GET"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method_not_allowed":{"summary":"The endpoint does not accept that method","value":{"error":{"code":"method_not_allowed","message":"This endpoint does not accept that method."}}}}}},"x-error-codes":["method_not_allowed"]},"429":{"description":"Too many requests with this key (Retry-After 60), or too many requests without a valid key from this address (Retry-After 600)","headers":{"Retry-After":{"description":"Seconds to wait before trying again","schema":{"type":"string"},"example":"60"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited_key":{"summary":"More than 120 requests with this key in a minute","value":{"error":{"code":"rate_limited","message":"Too many requests with this key. Wait a minute, then try again."}}},"rate_limited_ip":{"summary":"More than 30 requests without a valid key from this address in 10 minutes","value":{"error":{"code":"rate_limited","message":"Too many requests without a valid API key from this address. Try again after the time in the Retry-After header."}}}}}},"x-error-codes":["rate_limited"]},"503":{"description":"A database read failed; nothing was changed. Try again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unavailable":{"summary":"A database read failed; nothing was changed","value":{"error":{"code":"unavailable","message":"RowAttest could not complete this request just now. Try again in a moment."}}}}}},"x-error-codes":["unavailable"]}}}},"/api/v1/runs":{"post":{"summary":"Start a run, exactly as the dashboard's Run button does","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"The same key with the same body within 24 hours returns the first response.","example":"ci-build-4812"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["project_id"],"properties":{"project_id":{"type":"string","format":"uuid"}}},"example":{"project_id":"5eef4c90-9cac-46e3-9c9a-5b6d722b3952"}}}},"responses":{"202":{"description":"The run is queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunStarted"},"examples":{"queued":{"summary":"The run is queued","value":{"run_id":"2c949779-85a5-4fea-929c-4ebf99db6eaf","status":"queued","created_at":"2026-10-08T04:40:15.420704+00:00","poll_after_seconds":5}}}}}},"400":{"description":"The body has no valid project_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_body":{"summary":"The body has no valid project_id","value":{"error":{"code":"invalid_request","message":"The request needs a JSON body with a valid project_id."}}}}}},"x-error-codes":["invalid_request"]},"401":{"description":"Missing, malformed or unknown key","headers":{"WWW-Authenticate":{"description":"The scheme to use","schema":{"type":"string"},"example":"Bearer"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"Missing, malformed or unknown key","value":{"error":{"code":"unauthorized","message":"This request needs a valid RowAttest API key in the Authorization header."}}}}}},"x-error-codes":["unauthorized"]},"403":{"description":"The key's account has not accepted the Terms of Service: sign in at app.rowattest.com to accept them","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"consent_required":{"summary":"The key's account has not accepted the Terms of Service","value":{"error":{"code":"consent_required","message":"This account has not accepted the RowAttest Terms of Service. Sign in at app.rowattest.com to accept them, then try again."}}}}}},"x-error-codes":["consent_required"]},"404":{"description":"No such project for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"No such project or run for this key","value":{"error":{"code":"not_found","message":"No project or run with that id was found for this key."}}}}}},"x-error-codes":["not_found"]},"405":{"description":"The endpoint does not accept that method","headers":{"Allow":{"description":"The method this endpoint accepts","schema":{"type":"string"},"example":"POST"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method_not_allowed":{"summary":"The endpoint does not accept that method","value":{"error":{"code":"method_not_allowed","message":"This endpoint does not accept that method."}}}}}},"x-error-codes":["method_not_allowed"]},"409":{"description":"A run is already waiting, the project needs attention, its fit check has not completed or found no testable table, or the idempotency key was used for a different request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"run_in_progress":{"summary":"The project already has a run waiting to start","value":{"error":{"code":"run_in_progress","message":"This project already has a run waiting to start. Wait for it, then try again.","run_id":"2c949779-85a5-4fea-929c-4ebf99db6eaf"}}},"project_not_ready":{"summary":"The project needs attention on the dashboard","value":{"error":{"code":"project_not_ready","message":"This project needs attention on your dashboard before it can run.","reason":"database_password_required","url":"https://app.rowattest.com/dashboard"}}},"fit_check_pending":{"summary":"The project's fit check is still running","value":{"error":{"code":"project_not_ready","message":"RowAttest has not finished checking which tables it can test in this project; try again when the fit check on your dashboard is complete.","reason":"fit_check_pending","url":"https://app.rowattest.com/dashboard","poll_after_seconds":15}}},"fit_check_needed":{"summary":"No fit check of the project has completed","value":{"error":{"code":"project_not_ready","message":"RowAttest has not checked which tables it can test in this project; run a fit check on your dashboard, then try again.","reason":"fit_check_needed","url":"https://app.rowattest.com/dashboard"}}},"project_not_testable":{"summary":"The project's latest fit check found no table RowAttest can test","value":{"error":{"code":"project_not_testable","message":"RowAttest cannot test this project yet because its latest fit check found no table RowAttest can test; the dashboard lists each table and the reason.","url":"https://app.rowattest.com/dashboard"}}},"idempotency_key_reused":{"summary":"The idempotency key was used for a different request","value":{"error":{"code":"idempotency_key_reused","message":"This idempotency key was already used for a different request."}}}}}},"x-error-codes":["run_in_progress","project_not_ready","project_not_testable","idempotency_key_reused"]},"429":{"description":"Too many requests with this key (Retry-After 60), too many requests without a valid key from this address (Retry-After 600), too many run starts for the project (Retry-After 3600), or the month's run allowance is used up (10 runs a month on the free tier; it resets at the start of next month, UTC)","headers":{"Retry-After":{"description":"Seconds to wait before trying again","schema":{"type":"string"},"example":"3600"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited_key":{"summary":"More than 120 requests with this key in a minute","value":{"error":{"code":"rate_limited","message":"Too many requests with this key. Wait a minute, then try again."}}},"rate_limited_ip":{"summary":"More than 30 requests without a valid key from this address in 10 minutes","value":{"error":{"code":"rate_limited","message":"Too many requests without a valid API key from this address. Try again after the time in the Retry-After header."}}},"rate_limited_project":{"summary":"More than 10 run starts for this project in an hour","value":{"error":{"code":"rate_limited","message":"Too many run starts for this project in the last hour. Try again after the time in the Retry-After header."}}},"run_limit_reached_free":{"summary":"The free tier's monthly run allowance is used up","value":{"error":{"code":"run_limit_reached","message":"Free tier includes 10 runs per calendar month. You have used 10. Your allowance resets at the start of next month (UTC). A signed report for a complete run is bought on that run's page at app.rowattest.com, never through the API.","allowance":10,"used":10,"resets_at":"2026-11-01T00:00:00Z","url":"https://app.rowattest.com/dashboard"}}},"run_limit_reached":{"summary":"The month's run allowance is used up (allowance above the free tier)","value":{"error":{"code":"run_limit_reached","message":"Pro includes 200 runs per calendar month. You have used 200. Your allowance resets at the start of next month (UTC)."}}}}}},"x-error-codes":["rate_limited","run_limit_reached"]},"503":{"description":"A database read failed; nothing was changed. Try again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unavailable":{"summary":"A database read failed; nothing was changed","value":{"error":{"code":"unavailable","message":"RowAttest could not complete this request just now. Try again in a moment."}}}}}},"x-error-codes":["unavailable"]}}}},"/api/v1/runs/{id}":{"get":{"summary":"A run's status","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"example":"2c949779-85a5-4fea-929c-4ebf99db6eaf"},{"name":"wait","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":60},"description":"Wait up to this many seconds for the status to change, then answer either way.","example":30}],"responses":{"200":{"description":"The run","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Run"},"examples":{"running":{"summary":"Running, at stage 4","value":{"run_id":"2c949779-85a5-4fea-929c-4ebf99db6eaf","project_id":"5eef4c90-9cac-46e3-9c9a-5b6d722b3952","status":"running","stage":4,"created_at":"2026-10-08T04:40:15.420704+00:00","started_at":"2026-10-08T04:40:15.430436+00:00","finished_at":null,"report_id":null,"verify_url":null,"poll_after_seconds":10,"access_model_sha256":null}},"complete":{"summary":"Complete, with its signed report","value":{"run_id":"2c949779-85a5-4fea-929c-4ebf99db6eaf","project_id":"5eef4c90-9cac-46e3-9c9a-5b6d722b3952","status":"complete","stage":6,"created_at":"2026-10-08T04:40:15.420704+00:00","started_at":"2026-10-08T04:40:15.430436+00:00","finished_at":"2026-10-08T04:40:16.908+00:00","report_id":"RA-01M4CWZSH7Y5NT4R665MFZ50G8","verify_url":"https://rowattest.com/verify/RA-01M4CWZSH7Y5NT4R665MFZ50G8","poll_after_seconds":null,"access_model_sha256":"e118eb5cc732c8e5847df356d08cf0aa07b3653c9e9351a5ad0ee18f4300fe7c"}}}}}},"400":{"description":"wait is not a whole number from 1 to 60","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_wait":{"summary":"wait is not a whole number from 1 to 60","value":{"error":{"code":"invalid_request","message":"The wait value must be a whole number from 1 to 60."}}}}}},"x-error-codes":["invalid_request"]},"401":{"description":"Missing, malformed or unknown key","headers":{"WWW-Authenticate":{"description":"The scheme to use","schema":{"type":"string"},"example":"Bearer"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"Missing, malformed or unknown key","value":{"error":{"code":"unauthorized","message":"This request needs a valid RowAttest API key in the Authorization header."}}}}}},"x-error-codes":["unauthorized"]},"403":{"description":"The key's account has not accepted the Terms of Service: sign in at app.rowattest.com to accept them","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"consent_required":{"summary":"The key's account has not accepted the Terms of Service","value":{"error":{"code":"consent_required","message":"This account has not accepted the RowAttest Terms of Service. Sign in at app.rowattest.com to accept them, then try again."}}}}}},"x-error-codes":["consent_required"]},"404":{"description":"No such run for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"No such project or run for this key","value":{"error":{"code":"not_found","message":"No project or run with that id was found for this key."}}}}}},"x-error-codes":["not_found"]},"405":{"description":"The endpoint does not accept that method","headers":{"Allow":{"description":"The method this endpoint accepts","schema":{"type":"string"},"example":"GET"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method_not_allowed":{"summary":"The endpoint does not accept that method","value":{"error":{"code":"method_not_allowed","message":"This endpoint does not accept that method."}}}}}},"x-error-codes":["method_not_allowed"]},"429":{"description":"Too many requests with this key (Retry-After 60), or too many requests without a valid key from this address (Retry-After 600)","headers":{"Retry-After":{"description":"Seconds to wait before trying again","schema":{"type":"string"},"example":"60"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited_key":{"summary":"More than 120 requests with this key in a minute","value":{"error":{"code":"rate_limited","message":"Too many requests with this key. Wait a minute, then try again."}}},"rate_limited_ip":{"summary":"More than 30 requests without a valid key from this address in 10 minutes","value":{"error":{"code":"rate_limited","message":"Too many requests without a valid API key from this address. Try again after the time in the Retry-After header."}}}}}},"x-error-codes":["rate_limited"]},"503":{"description":"A database read failed; nothing was changed. Try again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unavailable":{"summary":"A database read failed; nothing was changed","value":{"error":{"code":"unavailable","message":"RowAttest could not complete this request just now. Try again in a moment."}}}}}},"x-error-codes":["unavailable"]}}}},"/api/v1/runs/{id}/findings":{"get":{"summary":"A finished run's findings","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"example":"2c949779-85a5-4fea-929c-4ebf99db6eaf"}],"responses":{"200":{"description":"The findings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Findings"},"examples":{"findings":{"summary":"Abridged: none failed, two allowed by the access model, eight provisional","value":{"run_id":"2c949779-85a5-4fea-929c-4ebf99db6eaf","verification":"signed","report_id":"RA-01M4CWZSH7Y5NT4R665MFZ50G8","verify_url":"https://rowattest.com/verify/RA-01M4CWZSH7Y5NT4R665MFZ50G8","counts":{"pass":109,"fail":0,"provisional":8,"not_evaluated":4,"error":0,"allowed_by_model":2},"summary":{"disagreement":0,"error":0,"layer_a_only":4,"layer_b_corroborated":4,"leak":0,"model_divergence":0,"pass":109,"total":119},"clean_report_blocked":false,"findings":[{"cell_id":"c003","verdict":"ALLOWED-BY-MODEL","surface":"table:public.categories","operation":"select","boundary":"model-open","identity":{"label":"member of another organization","principal":"other","role":"authenticated"},"layer_a":"allowed","layer_b":"allowed","note":"Access allowed by the access model: both test paths observed \"allowed\". Not counted as PASS.","identifiers":[]},{"cell_id":"c113","verdict":"layer-b-corroborated-provisional","surface":"bucket:tasklio-files","operation":"storage-read","boundary":"shared","identity":{"label":"owner","principal":"owner","role":"authenticated"},"layer_a":"inconclusive","layer_b":"denied","note":"Layer A inconclusive — not_found envelope is indistinguishable from an absent object (L48): the front door cannot attribute the refusal to row-level security; body={\"statusCode\":\"404\",\"error\":\"not_found\",\"message\":\"Object not found\"}. Layer B determinate: \"denied\". Corroborated by one layer only — provisional; a single observation never earns an agreed verdict.","identifiers":[]},{"cell_id":"c114","verdict":"layer-a-only-provisional","surface":"bucket:tasklio-files","operation":"storage-write","boundary":"shared","identity":{"label":"owner","principal":"owner","role":"authenticated"},"layer_a":"denied","layer_b":null,"note":"Layer B could not faithfully simulate this cell (storage write); Layer A alone observed \"denied\" — provisional, no agreed verdict.","identifiers":[]}],"allowed_by_model":[{"cell_id":"c003","surface":"table:public.categories","operation":"select","identity":{"label":"member of another organization","principal":"other","role":"authenticated"},"layer_a":"allowed","layer_b":"allowed","note":"Access allowed by the access model: both test paths observed \"allowed\". Not counted as PASS."}],"flags":[{"class":"C1","kind":"bypassrls-role","subject":"postgres","explanation":"Role postgres has BYPASSRLS, so row-level security never applies to it.","identifiers":[]},{"class":"C3","kind":"unconstrained-tenant-write","subject":"public.task_flags.task_flags_update_own","explanation":"The UPDATE policy task_flags_update_own does not limit the organization column org_id in its write check (WITH CHECK), so a write it allows can move the row to another organization.","identifiers":[{"id":"CWE-639","name":"Authorization Bypass Through User-Controlled Key","scheme":"CWE"},{"id":"API1:2023","name":"Broken Object Level Authorization","scheme":"OWASP-API-Security-2023"}]}],"blocking_findings":[]}},"unverified":{"summary":"Abridged: the same run before it had a signed report (verification unverified)","value":{"run_id":"2c949779-85a5-4fea-929c-4ebf99db6eaf","verification":"unverified","report_id":null,"verify_url":null,"url":"https://app.rowattest.com/runs/2c949779-85a5-4fea-929c-4ebf99db6eaf","counts":{"pass":109,"fail":0,"provisional":8,"not_evaluated":null,"error":0,"allowed_by_model":2},"findings":[{"verdict":"ALLOWED-BY-MODEL","surface":"table:public.categories","operation":"select","principal":"other"},{"verdict":"layer-b-corroborated-provisional","surface":"bucket:tasklio-files","operation":"storage-read","principal":"owner"},{"verdict":"layer-a-only-provisional","surface":"bucket:tasklio-files","operation":"storage-write","principal":"owner"}],"findings_lines":["C3","unconstrained tenant write: public.task_flags.task_flags_update_own"],"coverage":["This report covers 7 of 8 tables. 1 table was not evaluated; the full report lists it with its reason.","Basis: 9 detected by RowAttest, 0 confirmed by the customer, 0 declared by the customer.","Access model fingerprint (SHA-256): e118eb5cc732c8e5847df356d08cf0aa07b3653c9e9351a5ad0ee18f4300fe7c","119 of 123 boundary checks were executable; 4 were not evaluated.","A PASS applies only to the checks listed here, at the time of this run, against the schema as it existed then. It does not certify that your application is secure."],"access_model":{"sha256":"e118eb5cc732c8e5847df356d08cf0aa07b3653c9e9351a5ad0ee18f4300fe7c","version":null,"basis_counts":{"detected":9,"confirmed":0,"declared":0},"tables":{"covered":7,"declared_public":0,"not_evaluated":1,"total":8}}}}}}}},"401":{"description":"Missing, malformed or unknown key","headers":{"WWW-Authenticate":{"description":"The scheme to use","schema":{"type":"string"},"example":"Bearer"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"Missing, malformed or unknown key","value":{"error":{"code":"unauthorized","message":"This request needs a valid RowAttest API key in the Authorization header."}}}}}},"x-error-codes":["unauthorized"]},"403":{"description":"The key's account has not accepted the Terms of Service: sign in at app.rowattest.com to accept them","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"consent_required":{"summary":"The key's account has not accepted the Terms of Service","value":{"error":{"code":"consent_required","message":"This account has not accepted the RowAttest Terms of Service. Sign in at app.rowattest.com to accept them, then try again."}}}}}},"x-error-codes":["consent_required"]},"404":{"description":"No such run for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"No such project or run for this key","value":{"error":{"code":"not_found","message":"No project or run with that id was found for this key."}}}}}},"x-error-codes":["not_found"]},"405":{"description":"The endpoint does not accept that method","headers":{"Allow":{"description":"The method this endpoint accepts","schema":{"type":"string"},"example":"GET"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method_not_allowed":{"summary":"The endpoint does not accept that method","value":{"error":{"code":"method_not_allowed","message":"This endpoint does not accept that method."}}}}}},"x-error-codes":["method_not_allowed"]},"409":{"description":"Not finished yet, the signed report is being prepared, or there is none","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"run_not_finished":{"summary":"The run has not finished","value":{"error":{"code":"run_not_finished","message":"This run has not finished yet. Try again when its status is complete.","status":"running"}}},"report_not_ready":{"summary":"The signed report is being prepared","value":{"error":{"code":"report_not_ready","message":"The signed report for this run is being prepared. Try again in a few seconds.","poll_after_seconds":5}}},"report_unavailable":{"summary":"The run has no signed report","value":{"error":{"code":"report_unavailable","message":"This run has no signed report.","url":"https://app.rowattest.com/runs/2c949779-85a5-4fea-929c-4ebf99db6eaf"}}},"report_unavailable_failed":{"summary":"The run failed, so it has no signed report","value":{"error":{"code":"report_unavailable","message":"This run has no signed report.","url":"https://app.rowattest.com/runs/2c949779-85a5-4fea-929c-4ebf99db6eaf","status":"failed"}}}}}},"x-error-codes":["run_not_finished","report_not_ready","report_unavailable"]},"429":{"description":"Too many requests with this key (Retry-After 60), or too many requests without a valid key from this address (Retry-After 600)","headers":{"Retry-After":{"description":"Seconds to wait before trying again","schema":{"type":"string"},"example":"60"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited_key":{"summary":"More than 120 requests with this key in a minute","value":{"error":{"code":"rate_limited","message":"Too many requests with this key. Wait a minute, then try again."}}},"rate_limited_ip":{"summary":"More than 30 requests without a valid key from this address in 10 minutes","value":{"error":{"code":"rate_limited","message":"Too many requests without a valid API key from this address. Try again after the time in the Retry-After header."}}}}}},"x-error-codes":["rate_limited"]},"503":{"description":"A database read failed; nothing was changed. Try again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unavailable":{"summary":"A database read failed; nothing was changed","value":{"error":{"code":"unavailable","message":"RowAttest could not complete this request just now. Try again in a moment."}}}}}},"x-error-codes":["unavailable"]}}}},"/api/v1/runs/{id}/report":{"get":{"summary":"A finished run's signed full report: the canonical JSON, byte for byte as stored","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"example":"2c949779-85a5-4fea-929c-4ebf99db6eaf"}],"responses":{"200":{"description":"The signed report","content":{"application/json":{"schema":{"type":"object"},"examples":{"report":{"summary":"Abridged: the real report carries every section; verify it at verify_url or offline","value":{"schema":"rowattest.attestation-report","schema_version":"2.0","report_id":"RA-01M4CWZSH7Y5NT4R665MFZ50G8","engine":{"name":"rowattest-engine","version":"0.1.0"},"run":{"run_id":"4880bc78","started_at":"2026-10-08T04:40:15.559Z","generated_at":"2026-10-08T04:40:16.882Z"},"access_model":{"sha256":"e118eb5cc732c8e5847df356d08cf0aa07b3653c9e9351a5ad0ee18f4300fe7c","version":null,"basis_counts":{"confirmed":0,"declared":0,"detected":9},"tables":{"covered":7,"declared_public":0,"not_evaluated":1,"not_evaluated_tables":[{"reason":"The access model marks this table as not sure, so it is listed but not tested.","reason_code":"UNSURE","table":"public.task_flags"}],"total":8}},"differential":{"summary":{"disagreement":0,"error":0,"layer_a_only":4,"layer_b_corroborated":4,"leak":0,"model_divergence":0,"pass":109,"total":119}},"signature":{"algorithm":"Ed25519","report_sha256":"893b79eb375616101ffe82e5d7056daab1def43fb47c927452059442c9c300a4","signed_at":"2026-10-08T04:40:17.706Z","verify_url":"https://rowattest.com/verify/RA-01M4CWZSH7Y5NT4R665MFZ50G8"}}}}}}},"401":{"description":"Missing, malformed or unknown key","headers":{"WWW-Authenticate":{"description":"The scheme to use","schema":{"type":"string"},"example":"Bearer"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"Missing, malformed or unknown key","value":{"error":{"code":"unauthorized","message":"This request needs a valid RowAttest API key in the Authorization header."}}}}}},"x-error-codes":["unauthorized"]},"403":{"description":"The key's account has not accepted the Terms of Service: sign in at app.rowattest.com to accept them","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"consent_required":{"summary":"The key's account has not accepted the Terms of Service","value":{"error":{"code":"consent_required","message":"This account has not accepted the RowAttest Terms of Service. Sign in at app.rowattest.com to accept them, then try again."}}}}}},"x-error-codes":["consent_required"]},"404":{"description":"No such run for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"No such project or run for this key","value":{"error":{"code":"not_found","message":"No project or run with that id was found for this key."}}}}}},"x-error-codes":["not_found"]},"405":{"description":"The endpoint does not accept that method","headers":{"Allow":{"description":"The method this endpoint accepts","schema":{"type":"string"},"example":"GET"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method_not_allowed":{"summary":"The endpoint does not accept that method","value":{"error":{"code":"method_not_allowed","message":"This endpoint does not accept that method."}}}}}},"x-error-codes":["method_not_allowed"]},"409":{"description":"Not finished yet, the signed report is being prepared, or there is none","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"run_not_finished":{"summary":"The run has not finished","value":{"error":{"code":"run_not_finished","message":"This run has not finished yet. Try again when its status is complete.","status":"running"}}},"report_not_ready":{"summary":"The signed report is being prepared","value":{"error":{"code":"report_not_ready","message":"The signed report for this run is being prepared. Try again in a few seconds.","poll_after_seconds":5}}},"report_unavailable":{"summary":"The run has no signed report","value":{"error":{"code":"report_unavailable","message":"This run has no signed report.","url":"https://app.rowattest.com/runs/2c949779-85a5-4fea-929c-4ebf99db6eaf"}}},"report_unavailable_failed":{"summary":"The run failed, so it has no signed report","value":{"error":{"code":"report_unavailable","message":"This run has no signed report.","url":"https://app.rowattest.com/runs/2c949779-85a5-4fea-929c-4ebf99db6eaf","status":"failed"}}}}}},"x-error-codes":["run_not_finished","report_not_ready","report_unavailable"]},"429":{"description":"Too many requests with this key (Retry-After 60), or too many requests without a valid key from this address (Retry-After 600)","headers":{"Retry-After":{"description":"Seconds to wait before trying again","schema":{"type":"string"},"example":"60"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited_key":{"summary":"More than 120 requests with this key in a minute","value":{"error":{"code":"rate_limited","message":"Too many requests with this key. Wait a minute, then try again."}}},"rate_limited_ip":{"summary":"More than 30 requests without a valid key from this address in 10 minutes","value":{"error":{"code":"rate_limited","message":"Too many requests without a valid API key from this address. Try again after the time in the Retry-After header."}}}}}},"x-error-codes":["rate_limited"]},"503":{"description":"A database read failed; nothing was changed. Try again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unavailable":{"summary":"A database read failed; nothing was changed","value":{"error":{"code":"unavailable","message":"RowAttest could not complete this request just now. Try again in a moment."}}}}}},"x-error-codes":["unavailable"]}}}}}}