{"openapi":"3.1.0","info":{"title":"Tray","version":"0.1.0","description":"Self-hosted Tray API: publish folders as sites on your own domain. Authenticate with `authorization: Bearer tray_k_…`. Errors are `{ error, code, hint?, param? }` with snake_case codes."},"servers":[{"url":"https://tray.{domain}/v1","description":"Your tray","variables":{"domain":{"default":"thts.app","description":"The domain the tray was set up on"}}}],"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"An API key, `tray_k_…`"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"hint":{"type":"string"},"param":{"type":"string"},"missing":{"type":"array","items":{"type":"string"}}},"required":["error","code"],"additionalProperties":{}},"Blob":{"type":"object","properties":{"hash":{"type":"string","pattern":"^[a-f0-9]{64}$"},"size":{"type":"integer","minimum":0}},"required":["hash","size"]},"Site":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":"string","format":"uri"},"visibility":{"type":"string","enum":["public","private"]},"currentDeploy":{"type":["string","null"]},"domains":{"type":"array","items":{"type":"string"}},"shareCount":{"type":"integer","minimum":0},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["name","url","visibility","currentDeploy","domains","shareCount","createdAt","updatedAt"]},"Deploy":{"type":"object","properties":{"id":{"type":"string"},"files":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"spa":{"type":"boolean"},"createdAt":{"type":"string"},"createdBy":{"type":"string"}},"required":["id","files","bytes","spa","createdAt","createdBy"]},"Share":{"type":"object","properties":{"entry":{"type":"string"},"owner":{"type":"boolean"}},"required":["entry","owner"]},"Domain":{"type":"object","properties":{"host":{"type":"string"},"site":{"type":"string"},"createdAt":{"type":"string"}},"required":["host","site","createdAt"]},"Key":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-f0-9]{12}$"},"name":{"type":"string"},"email":{"type":"string"},"createdAt":{"type":"string"},"lastUsedAt":{"type":["string","null"]}},"required":["id","name","email","createdAt","lastUsedAt"]},"DeviceCode":{"type":"object","properties":{"deviceCode":{"type":"string"},"userCode":{"type":"string"},"verificationUri":{"type":"string","format":"uri"},"verificationUriComplete":{"type":"string","format":"uri"},"expiresIn":{"type":"integer","exclusiveMinimum":0},"interval":{"type":"integer","exclusiveMinimum":0}},"required":["deviceCode","userCode","verificationUri","verificationUriComplete","expiresIn","interval"]}},"parameters":{}},"paths":{"/blobs/check":{"post":{"operationId":"checkBlobs","summary":"Find which blobs are missing","description":"Takes up to 512 sha256 hashes and returns the ones not yet stored. Upload those with `PUT /blobs/{sha256}` before deploying.","security":[{"bearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hashes":{"type":"array","items":{"type":"string","pattern":"^[a-f0-9]{64}$"},"maxItems":512}},"required":["hashes"]}}}},"responses":{"200":{"description":"Hashes not yet stored","content":{"application/json":{"schema":{"type":"object","properties":{"missing":{"type":"array","items":{"type":"string","pattern":"^[a-f0-9]{64}$"}}},"required":["missing"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/blobs/{sha256}":{"put":{"operationId":"uploadBlob","summary":"Upload a blob","description":"Raw file bytes as the body; `content-length` is required (411 without it, 413 over 50 MB). The body must hash to `sha256` (400 `digest_mismatch`). 201 when stored, 200 when it already existed.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-f0-9]{64}$"},"required":true,"name":"sha256","in":"path"}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Already stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Blob"}}}},"201":{"description":"Stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Blob"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"411":{"description":"content-length required (`length_required`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Too large (`too_large`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sites":{"get":{"operationId":"listSites","summary":"List sites","description":"Every site with its URL, visibility, current deploy and custom domains, most recently updated first.","security":[{"bearer":[]}],"responses":{"200":{"description":"Sites, most recently updated first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Site"}}},"required":["data"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sites/{site}":{"get":{"operationId":"getSite","summary":"Get a site","description":"One site's URL, visibility, current deploy, custom domains and share count.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"},"required":true,"name":"site","in":"path"}],"responses":{"200":{"description":"The site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Site"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateSite","summary":"Change a site's visibility","description":"`private` limits it to the owner and its share list; `public` makes it visible to anyone with the link. The share list is kept either way.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"},"required":true,"name":"site","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"visibility":{"type":"string","enum":["public","private"]}},"required":["visibility"]}}}},"responses":{"200":{"description":"The updated site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Site"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteSite","summary":"Remove a site","description":"Deletes the site, its deploy records and its custom domains. Blobs are left for later garbage collection.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"},"required":true,"name":"site","in":"path"}],"responses":{"204":{"description":"Removed"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Upstream (Cloudflare API) error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sites/{site}/deploys":{"post":{"operationId":"createDeploy","summary":"Deploy a manifest","description":"Creates the site if needed and makes the manifest live. Returns 200 with the current deploy when `files` and `spa` are unchanged, 409 `site_exists` when the site was last put from another `source` (pass `replace: true`), and 409 `missing_blobs` with `missing` when blobs need uploading first.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"},"required":true,"name":"site","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"files":{"type":"object","additionalProperties":{"type":"object","properties":{"hash":{"type":"string","pattern":"^[a-f0-9]{64}$"},"size":{"type":"integer","minimum":0},"type":{"type":"string","maxLength":255}},"required":["hash","size","type"]}},"spa":{"type":"boolean"},"visibility":{"type":"string","enum":["public","private"]},"replace":{"type":"boolean"},"source":{"type":"object","properties":{"path":{"type":"string","minLength":1},"machine":{"type":"string","minLength":1}},"required":["path","machine"]}},"required":["files","spa","source"]}}}},"responses":{"200":{"description":"Unchanged: the manifest equals the current deploy","content":{"application/json":{"schema":{"type":"object","properties":{"site":{"$ref":"#/components/schemas/Site"},"deploy":{"$ref":"#/components/schemas/Deploy"}},"required":["site","deploy"]}}}},"201":{"description":"New deploy","content":{"application/json":{"schema":{"type":"object","properties":{"site":{"$ref":"#/components/schemas/Site"},"deploy":{"$ref":"#/components/schemas/Deploy"}},"required":["site","deploy"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Too large (`too_large`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listDeploys","summary":"List deploys","description":"Newest first. Page with `limit` (default 20, max 100) and `before=<deployId>`; `hasMore` says whether older deploys exist.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"},"required":true,"name":"site","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"}],"responses":{"200":{"description":"A page of deploys","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Deploy"}},"hasMore":{"type":"boolean"}},"required":["data","hasMore"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sites/{site}/rollback":{"post":{"operationId":"rollbackSite","summary":"Roll a site back to a deploy","description":"Makes deploy `to` live. `to` is required; list deploys to pick one.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"},"required":true,"name":"site","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string","minLength":1}},"required":["to"]}}}},"responses":{"200":{"description":"The site, now serving `to`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Site"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sites/{site}/shares":{"get":{"operationId":"listShares","summary":"List who can see a private site","description":"The owner first, then shared emails and `@domain` entries. 409 `site_public` on a public site.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"},"required":true,"name":"site","in":"path"}],"responses":{"200":{"description":"Owner and share entries","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Share"}}},"required":["data"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addShares","summary":"Share a private site","description":"Adds emails or `@domain` entries (anyone with an email at that domain). Entries already present are ignored. Returns the full list.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"},"required":true,"name":"site","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"anyOf":[{"type":"string","format":"email"},{"type":"string"}]},"minItems":1,"maxItems":100}},"required":["entries"]}}}},"responses":{"200":{"description":"The full share list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Share"}}},"required":["data"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sites/{site}/shares/{entry}":{"delete":{"operationId":"removeShare","summary":"Stop sharing a private site","description":"Removes an email or `@domain` entry. Removing an entry that isn't shared is a no-op; the owner can't be removed.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"},"required":true,"name":"site","in":"path"},{"schema":{"anyOf":[{"type":"string","format":"email"},{"type":"string"}]},"required":true,"name":"entry","in":"path"}],"responses":{"204":{"description":"Removed (or wasn't shared)"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/domains":{"get":{"operationId":"listDomains","summary":"List custom domains","description":"All custom hosts and the sites they serve, sorted by host. Filter with `?site=`.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"},"required":false,"name":"site","in":"query"}],"responses":{"200":{"description":"Custom domains, by host","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Domain"}}},"required":["data"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addDomain","summary":"Attach a custom domain to a site","description":"Adds the host as a Worker Custom Domain (Cloudflare creates DNS and TLS) and to the sign-in Access app. The host's zone must be on the same Cloudflare account (422 `zone_not_in_account`). 200 if already mapped to this site, 409 `domain_taken` if mapped elsewhere.","security":[{"bearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"host":{"type":"string","maxLength":253},"site":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"}},"required":["host","site"]}}}},"responses":{"200":{"description":"Already mapped to this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"}}}},"201":{"description":"Attached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Unprocessable (`zone_not_in_account`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Upstream (Cloudflare API) error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/domains/{host}":{"delete":{"operationId":"removeDomain","summary":"Detach a custom domain","description":"Removes the Worker Custom Domain, the sign-in Access entry and the host mapping.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","maxLength":253},"required":true,"name":"host","in":"path"}],"responses":{"204":{"description":"Detached"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Upstream (Cloudflare API) error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/keys":{"get":{"operationId":"listKeys","summary":"List API keys","description":"Every key's id, name, owner email and timestamps. Never returns key secrets.","security":[{"bearer":[]}],"responses":{"200":{"description":"Keys, oldest first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Key"}}},"required":["data"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createKey","summary":"Create an API key","description":"For agents and CI. The `key` is returned only in this response; only its hash is stored.","security":[{"bearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}]+$/u"}},"required":["name"]}}}},"responses":{"201":{"description":"The new key","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-f0-9]{12}$"},"name":{"type":"string"},"key":{"type":"string"}},"required":["id","name","key"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/keys/current":{"delete":{"operationId":"revokeCurrentKey","summary":"Revoke the calling key","description":"Logout: revokes the API key that authenticated this request.","security":[{"bearer":[]}],"responses":{"204":{"description":"Revoked"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/keys/{id}":{"delete":{"operationId":"revokeKey","summary":"Revoke an API key","description":"Revokes a key by its 12-character id.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-f0-9]{12}$"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Revoked"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/device":{"post":{"operationId":"startDeviceLogin","summary":"Start a device login","description":"No auth. Returns a device code for the CLI to poll with and a user code for the owner to approve at `verificationUriComplete` or with `tray approve`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}]+$/u"}},"required":["name"]}}}},"responses":{"200":{"description":"Codes for the CLI and the approver","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceCode"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/token":{"post":{"operationId":"pollDeviceToken","summary":"Poll for a device login's API key","description":"No auth. Returns 400 `authorization_pending` until approved, `slow_down` when polling faster than `interval`, `expired_token` when the code expired or was used, or `access_denied` when denied. On approval returns the key once.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"deviceCode":{"type":"string","minLength":1}},"required":["deviceCode"]}}}},"responses":{"200":{"description":"Approved: the key, shown once","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"}},"required":["key","email","name"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/device/{userCode}":{"get":{"operationId":"getDeviceLogin","summary":"Look up a pending device login","description":"Returns the device name and IP for a user code so `tray approve` can show them before approving.","security":[{"bearer":[]}],"parameters":[{"schema":{"type":"string","minLength":8,"maxLength":9},"required":true,"name":"userCode","in":"path"}],"responses":{"200":{"description":"The pending login","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"ip":{"type":"string"},"expiresAt":{"type":"string"}},"required":["name","ip","expiresAt"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/approve":{"post":{"operationId":"approveDeviceLogin","summary":"Approve or deny a device login","description":"Mints an API key bound to the approver's email and the device name. `approve: false` denies. Accepts an API key or the owner's Cloudflare Access identity. Repeating the same decision is a no-op.","security":[{"bearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userCode":{"type":"string","minLength":8,"maxLength":9},"approve":{"type":"boolean"}},"required":["userCode"]}}}},"responses":{"204":{"description":"Done"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key (`unauthenticated`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}