Authentication

Send an API key as a bearer token. Make one with tray key create.

curl https://tray.acme.dev/v1/sites \    -H "authorization: Bearer $TRAY_API_KEY"

Publishing

Uploads are content-addressed, so unchanged files are never sent twice. Publishing takes four steps.

1 Hash SHA-256 every file.
2 POST /blobs/check Send up to 512 hashes. Get back the missing ones.
3 PUT /blobs/<hash> Upload each missing file.
4 POST /sites/<site>/deploys Send the file list. Get the URL.
REQUEST · POST /sites/q3-review/deploys{  "files": {    "/index.html": {      "hash": "9f86d081884c7d65…",      "size": 5120,      "type": "text/html"    }  },  "spa": false,  "source": {    "path": "/Users/sid/work/q3-review",    "machine": "mbp-3f9a"  }}
RESPONSE · 201{  "site": {    "name": "q3-review",    "url": "https://q3-review.acme.dev",    "visibility": "public",    "currentDeploy": "lq3k2-8f1a",    "domains": [],    "shareCount": 1,    "createdAt": "2026-10-26T09:12:03Z",    "updatedAt": "2026-10-26T09:12:03Z"  },  "deploy": {    "id": "lq3k2-8f1a",    "files": 1,    "bytes": 5120,    "spa": false,    "createdAt": "2026-10-26T09:12:03Z",    "createdBy": "sid@acme.com"  }}

Endpoints

Sites

POST /blobs/check Which hashes are missing. Up to 512.
PUT /blobs/<sha256> Upload one file. Checked against its hash.
POST /sites/<site>/deploys Publish a site. Returns the URL.
GET /sites Your sites.
GET /sites/<site> One site.
PATCH /sites/<site> Change visibility.
DELETE /sites/<site> Remove a site.
GET /sites/<site>/deploys Recent versions.
POST /sites/<site>/rollback Go back a version.

Sharing, domains, keys

GET /sites/<site>/shares Who can see a private site. POST to add, DELETE to remove.
GET /domains Custom domains. POST to add, DELETE to remove.
GET /keys API keys. POST to create, DELETE to revoke.

Login

POST /auth/device Start a login. Returns a code. No key.
POST /auth/token Poll for the key once approved. No key.
GET /auth/device/<code> Who is asking, before you approve.
POST /auth/approve Approve a code.

Errors

Errors return a status code and a JSON body.

RESPONSE · 409{  "error": "q3-review already exists",  "code": "site_exists",  "hint": "Pass replace: true, or use another name.",  "param": "site"}
400 Bad input. param names the field.
401 Missing or wrong key.
404 No such site, domain or key.
409 Name taken. Code site_exists.
413 Over 50 MB.