{"openapi":"3.1.0","info":{"title":"Anvil API","version":"1.0.0","summary":"Premium on-device testing for iOS, Android, macOS, visionOS, watchOS.","description":"The Anvil REST API powers the dashboard at\n[anvil.koydo.app](https://anvil.koydo.app) and is also the integration\nsurface for CI systems, IDE plugins, and the `anvil` CLI.\n\n**Authentication.** Browser-initiated calls use the Supabase session\ncookie set by the dashboard sign-in flow. Programmatic callers use an\nAPI key minted at `POST /v1/org/api-keys`; pass it as\n`Authorization: Bearer <key>` on every subsequent request.\n\n**Versioning.** The URL path pins the major version (`/api/v1/…`).\nThe driver-side JSON-RPC protocol has its own semver ladder —\nsee `wiki/standards/protocol-versioning.md`.\n\n**Rate limits.** Default 120 req/min per org across the surface.\nDashboard routes (runs, devices) have a higher allowance; handshake\nendpoints are tighter.\n","contact":{"name":"Anvil team","email":"dev@anvil.koydo.app"},"license":{"name":"Proprietary — see LICENSE"}},"servers":[{"url":"https://anvil.koydo.app/api/v1","description":"Production"},{"url":"http://localhost:3000/api/v1","description":"Local dev (next dev)"}],"tags":[{"name":"Runs","description":"Create and inspect test runs."},{"name":"Devices","description":"List and register devices in your org."},{"name":"Audit log","description":"Org-scoped audit trail."},{"name":"API keys","description":"Programmatic access tokens."},{"name":"Auth","description":"Driver handshake + revocation (typically not called directly)."}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"anvil_live_xxx","description":"Mint at `POST /v1/org/api-keys`. The plaintext key is returned\nexactly once; store it in your CI's secret manager.\n"},"sessionCookie":{"type":"apiKey","in":"cookie","name":"sb-access-token","description":"Supabase session cookie — set automatically by the dashboard."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"unauthenticated"},"message":{"type":"string","example":"Sign in to continue."}}}}},"Device":{"type":"object","required":["id","orgId","name","status","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"orgId":{"type":"string","format":"uuid"},"name":{"type":"string","maxLength":120},"model":{"type":"string","nullable":true,"maxLength":120},"firmwareVersion":{"type":"string","nullable":true,"maxLength":120},"capabilities":{"type":"array","items":{"type":"string"}},"metadata":{"type":"object","additionalProperties":true,"nullable":true},"status":{"type":"string","enum":["provisioning","ready","offline","retired"]},"createdAt":{"type":"string","format":"date-time"}}},"DeviceRegisterBody":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"model":{"type":"string","maxLength":120},"firmwareVersion":{"type":"string","maxLength":120},"capabilities":{"type":"array","maxItems":64,"items":{"type":"string"}},"metadata":{"type":"object","additionalProperties":true}}},"Run":{"type":"object","required":["id","orgId","pipeline","status","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"orgId":{"type":"string","format":"uuid"},"pipeline":{"type":"string","maxLength":120},"deviceId":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["queued","running","passed","failed","cancelled"]},"metadata":{"type":"object","additionalProperties":true,"nullable":true},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string","nullable":true}}},"RunCreateBody":{"type":"object","required":["pipeline"],"properties":{"pipeline":{"type":"string","minLength":1,"maxLength":120},"deviceId":{"type":"string","format":"uuid"},"metadata":{"type":"object","additionalProperties":true}}},"Finding":{"type":"object","required":["id","runId","severity","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"runId":{"type":"string","format":"uuid"},"severity":{"type":"string","enum":["critical","high","medium","low","info"]},"category":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"detail":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"AuditEvent":{"type":"object","required":["id","orgId","action","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"orgId":{"type":"string","format":"uuid"},"actor":{"type":"string","nullable":true},"action":{"type":"string"},"resourceType":{"type":"string","nullable":true},"resourceId":{"type":"string","nullable":true},"metadata":{"type":"object","additionalProperties":true,"nullable":true},"ip":{"type":"string","nullable":true},"userAgent":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ApiKeyListed":{"type":"object","required":["id","name","prefix","scopes","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"prefix":{"type":"string","example":"anvil_live_"},"scopes":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true}}},"ApiKeyCreateBody":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"scopes":{"type":"array","maxItems":32,"items":{"type":"string","minLength":1,"maxLength":64}}}},"ApiKeyCreated":{"type":"object","required":["id","name","prefix","createdAt","plaintext","last4"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"prefix":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"plaintext":{"type":"string","description":"One-time plaintext key. Never stored; not returned again."},"last4":{"type":"string","minLength":4,"maxLength":4}}},"DeviceHandshakeBody":{"type":"object","required":["apiKey","device"],"properties":{"apiKey":{"type":"string"},"device":{"type":"object","required":["platform","udid","os_version"],"properties":{"platform":{"type":"string","enum":["ios","macos","visionos","watchos","android"]},"udid":{"type":"string"},"os_version":{"type":"string"},"bundle_id":{"type":"string"}}}}},"DeviceHandshakeResponse":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"Short-lived RS256 JWT (24h exp) used by the driver."},"expiresAt":{"type":"string","format":"date-time"}}}},"responses":{"Unauthenticated":{"description":"Sign in or pass a valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InvalidBody":{"description":"Request body failed zod validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"security":[{"apiKey":[]},{"sessionCookie":[]}],"paths":{"/devices":{"get":{"tags":["Devices"],"summary":"List devices in your org.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["devices"],"properties":{"devices":{"type":"array","items":{"$ref":"#/components/schemas/Device"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}},"post":{"tags":["Devices"],"summary":"Register a new device.","description":"Dashboard-initiated flow. On success returns the created device\nwith `status: \"provisioning\"`. Enforces the org's plan device cap;\nreturns **402 device_cap_reached** if exceeded.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceRegisterBody"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["device"],"properties":{"device":{"$ref":"#/components/schemas/Device"}}}}}},"400":{"$ref":"#/components/responses/InvalidBody"},"401":{"$ref":"#/components/responses/Unauthenticated"},"402":{"description":"Plan cap reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/runs":{"get":{"tags":["Runs"],"summary":"List the 50 most recent runs in your org.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["runs"],"properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/Run"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}},"post":{"tags":["Runs"],"summary":"Queue a new run.","description":"Returns 202 with the run in `status: \"queued\"`. A background\nworker picks it up asynchronously; poll `GET /runs/{id}` for\nprogress.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunCreateBody"}}}},"responses":{"202":{"description":"Accepted","content":{"application/json":{"schema":{"type":"object","required":["run"],"properties":{"run":{"$ref":"#/components/schemas/Run"}}}}}},"400":{"$ref":"#/components/responses/InvalidBody"},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/runs/{id}":{"get":{"tags":["Runs"],"summary":"Fetch a run's detail + findings.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["run","findings"],"properties":{"run":{"$ref":"#/components/schemas/Run"},"findings":{"type":"array","items":{"$ref":"#/components/schemas/Finding"}}}}}}},"400":{"description":"id must be a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/audit-log":{"get":{"tags":["Audit log"],"summary":"Paginated org-scoped audit events.","parameters":[{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"before","required":false,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["events"],"properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/AuditEvent"}},"nextCursor":{"type":"string","format":"date-time","nullable":true}}}}}},"400":{"description":"Invalid query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/org/api-keys":{"get":{"tags":["API keys"],"summary":"List your org's API keys.","description":"Prefix + last-used only; plaintext is never returned.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["keys"],"properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyListed"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"}}},"post":{"tags":["API keys"],"summary":"Mint a new API key.","description":"The `plaintext` field is returned exactly once — store it immediately.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateBody"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"}}}},"400":{"$ref":"#/components/responses/InvalidBody"},"401":{"$ref":"#/components/responses/Unauthenticated"}}}},"/org/api-keys/{id}":{"delete":{"tags":["API keys"],"summary":"Revoke an API key by id.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Revoked"},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/device/handshake":{"post":{"tags":["Auth"],"summary":"Driver handshake — mint a short-lived device JWT.","description":"Typically called by the `anvil` CLI / on-device driver, not by\nend users. Returns a JWT bound to the UDID with a 24-hour\nexpiry; rotate via `/auth/device/rotate` before it lapses.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceHandshakeBody"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceHandshakeResponse"}}}},"401":{"description":"Invalid/revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Plan device cap reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/device/rotate":{"post":{"tags":["Auth"],"summary":"Rotate a near-expiry device JWT.","security":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceHandshakeResponse"}}}},"401":{"description":"Expired or revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/revocation-list":{"get":{"tags":["Auth"],"summary":"Public revocation list for device JWTs.","description":"Returns the `jti` set of revoked device tokens. Drivers fetch\nthis periodically and refuse to run verbs if their own `jti`\nappears in the list.\n","security":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["jti"],"properties":{"jti":{"type":"array","items":{"type":"string"}},"generatedAt":{"type":"string","format":"date-time"}}}}}}}}}}}