Deploy
Publish inline JSON files or a tar.gz/zip archive.
GET /import/{token}
Does not consume the link. No-store and no-referrer; token authority, no cookies or API key on the page.
No authentication required.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| token | path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | Shared-theme HTML upload page |
| 404 | Link expired |
GET /v1/imports/status/{import_id}
Public by a random 128-bit ID; same-origin browsers only. Returns stages, counts and at most 10 plain warnings, with no owner, report, credentials or filenames. Finished imports expire after 24 hours; accounts retain at most 20 records. Usable links and receiving/queued/running imports retain their records. Expired unused links are swept. Status reads have a separate per-IP limiter (60 burst, one per second); retry after the Retry-After header on 429.
No authentication required.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| import_id | path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | Current progress |
| 404 | Not found |
| 429 | Too many status reads from this address |
POST /v1/imports/{token}
Same-origin browser upload, authorized by the one-time token from create_import_link. Checks the creating connection’s sign-in again. Streams up to 10 files to disk, sniffs XML, ZIP, TAR and gzip TAR, validates archive paths and byte limits, then converts and publishes asynchronously. Invalid files and a full queue leave the link usable. A reserved link returns 409. Publishing can replace the live version.
No authentication required.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| token | path | string | yes |
Request body: multipart/form-data
Responses:
| Status | Description |
|---|---|
| 202 | Files validated and import queued |
| 400 | Invalid request. A saved-data write whose JSON holds a NUL character (\u0000), half of a surrogate pair or bytes that are not UTF-8 is code invalid_json. |
| 403 | Sign-in ended (import_link_signed_out) or another browser origin |
| 404 | Import link expired |
| 409 | Already uploading |
| 413 | Payload too large (on saved data, code item_too_large; on a deploy: site_too_large (per-upload limit), site_total_too_large (whole website footprint for sites created from SITE_TOTAL_CAP_FROM), or account_storage_full (all account website files including Recently deleted). Totals count the separate live copy and kept versions after pruning; backend storage and saved data are excluded. The error names the limit and current size for total caps, and hint gives photo-compression and file-cleanup tips) |
| 429 | Too many uploads from this address |
| 503 | Busy; try again in a few minutes with the same link |
GET /v1/photo-links
The create_upload_link photos_url opens /photos?site=<name>#t=<token>. The page sends the fragment token in X-Photo-Token, never in a request URL or Referer, and immediately removes the fragment from browser history. No browser sign-in is needed. The link lasts 60 minutes for OAuth callers; API-key links are capped at the key’s known expiry (reported in expires_at), and permits up to 10 successful saves to one site. The creating OAuth grant (not the original access token) or API key is checked again on every request; only the site name is returned, with no account data or keys. The binding is to the site name for the link’s lifetime: renaming and recreating that name sends later photos to the replacement site within the same account. Apex Host and Origin checks and IP rate limits apply.
No authentication required.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| X-Photo-Token | header | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | The site this link can add photos to |
| 403 | Origin refused or creating sign-in revoked |
| 404 | This photo link has expired. Ask your chat for a new one. |
| 409 | A save is in progress |
| 429 | Rate limit exceeded |
POST /v1/photo-links
Uses the same X-Photo-Token, sign-in recheck, Origin policy, and rate limit as GET. Proxies only to POST /v1/sites/{sitename}/photos for the site fixed when the link was created. Query parameters cannot change the destination. The same multipart, body, photo, and storage caps apply, and all other live files are kept. Successful saves consume one of 10 uses; failed saves can be retried until expiry. X-Photo-Reconcile checks do not spend a use and remain available after the tenth save until link expiry. Short-lived credentials are revoked after each request and never leave the server. In-memory links expire on server restart.
No authentication required.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| X-Photo-Token | header | string | yes | |
| X-Photo-Reconcile | header | string, one of 1 |
Set to 1 after an uncertain save to check matching live photo bytes without writing a version. A missing match returns 409, never another upload. |
Request body: multipart/form-data
Responses:
| Status | Description |
|---|---|
| 200 | Photos added in one new live version, or matching photos confirmed without a write |
| 400 | Invalid request. A saved-data write whose JSON holds a NUL character (\u0000), half of a surrogate pair or bytes that are not UTF-8 is code invalid_json. |
| 401 | Missing or invalid API key — send the X-API-Key header, not Authorization Bearer. The body’s code is missing_api_key, wrong_auth_header, invalid_api_key, key_expired or key_expired_idle (create a new key from the Keys panel). |
| 403 | Origin or deploy permission refused |
| 404 | Site missing, or this photo link has expired. Ask your chat for a new one. |
| 409 | A save is in progress or a file/directory conflicts with the photo path |
| 413 | Photo |
| 429 | Rate limit or photo save capacity exceeded; Retry-After advises when to retry |
POST /v1/sites/{sitename}
Upload a .tar.gz or .zip (for framework builds, binary assets, large sites). Max 100 MB.
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| Idempotency-Key | header | string | Opaque key for a create request. For 24 hours, the same authenticated account, POST path and query, key and exact request body replay the original handler status and body, including errors, without executing it again. A different body under that key returns 409 idempotency_key_reused. Concurrent identical requests wait for the first response. Stored only in this server’s memory; a restart clears it. Authentication and rate limiting still apply to replays. A replay does not extend a minted link’s expiry; use a new key to request a new link. Storage row creates support owner credentials and signed-in visitors; anonymous writes ignore this header. Storage replay also checks current resource permissions and is scoped to the resolved site. | |
| author | query | string | Who made this version: free text, trimmed, control characters removed, at most 80 characters. Stored on the version and returned as author by GET /v1/sites/{sitename}/versions. |
Request body: application/gzip
Responses:
| Status | Description |
|---|---|
| 201 | Site created |
| 400 | Invalid request. A saved-data write whose JSON holds a NUL character (\u0000), half of a surrogate pair or bytes that are not UTF-8 is code invalid_json. |
| 401 | Missing or invalid API key — send the X-API-Key header, not Authorization Bearer. The body’s code is missing_api_key, wrong_auth_header, invalid_api_key, key_expired or key_expired_idle (create a new key from the Keys panel). |
| 409 | Conflict: the code says which (site_exists, domain_taken, append_only, custom_domain_required, not_an_object) |
| 413 | Payload too large (on saved data, code item_too_large; on a deploy: site_too_large (per-upload limit), site_total_too_large (whole website footprint for sites created from SITE_TOTAL_CAP_FROM), or account_storage_full (all account website files including Recently deleted). Totals count the separate live copy and kept versions after pruning; backend storage and saved data are excluded. The error names the limit and current size for total caps, and hint gives photo-compression and file-cleanup tips) |
PUT /v1/sites/{sitename}
Upload a new version of an existing site you own. Creates a new version and activates it; with ?publish=false the version is only stored (visitors keep seeing the live one) and the answer adds unpublished_version, preview_url and preview_expires_at. With ?create=1 a missing site is created instead (201, with the same checks as POST): one call from CI, e.g. curl -X PUT ".../v1/sites/<name>?create=1" -H "X-API-Key: $KEY" --data-binary @site.tar.gz.
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| publish | query | boolean | false stores the new version without making it live (updates only; a new site’s first version always goes live: 400) | |
| create | query | string, one of 1, 0, true, false |
1 creates the site when you have none of that name (reserved names, the site quota and publish=false are refused as on POST), so one PUT deploys from CI whether or not the site exists | |
| author | query | string | Who made this version: free text, trimmed, control characters removed, at most 80 characters. Stored on the version and returned as author by GET /v1/sites/{sitename}/versions. |
Request body: application/gzip
Responses:
| Status | Description |
|---|---|
| 200 | Site updated |
| 201 | Site created (?create=1 and no site of that name) |
| 401 | Missing or invalid API key — send the X-API-Key header, not Authorization Bearer. The body’s code is missing_api_key, wrong_auth_header, invalid_api_key, key_expired or key_expired_idle (create a new key from the Keys panel). |
| 404 | Not found |
| 413 | Payload too large (on saved data, code item_too_large; on a deploy: site_too_large (per-upload limit), site_total_too_large (whole website footprint for sites created from SITE_TOTAL_CAP_FROM), or account_storage_full (all account website files including Recently deleted). Totals count the separate live copy and kept versions after pruning; backend storage and saved data are excluded. The error names the limit and current size for total caps, and hint gives photo-compression and file-cleanup tips) |
POST /v1/sites/{sitename}/files
The LLM-friendly deploy path — send every file inline in one request, no archiving. Text goes in files; base64-encoded binary goes in files_base64. index.html is required. Relative paths only (../absolute are rejected); secret files (.env, .git/*, id_rsa) are dropped and script extensions (.sh .py .php …) are rejected. Use PUT to update an existing site (409 site_exists says so); PUT with ?create=1 creates or updates in one call.
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| Idempotency-Key | header | string | Opaque key for a create request. For 24 hours, the same authenticated account, POST path and query, key and exact request body replay the original handler status and body, including errors, without executing it again. A different body under that key returns 409 idempotency_key_reused. Concurrent identical requests wait for the first response. Stored only in this server’s memory; a restart clears it. Authentication and rate limiting still apply to replays. A replay does not extend a minted link’s expiry; use a new key to request a new link. Storage row creates support owner credentials and signed-in visitors; anonymous writes ignore this header. Storage replay also checks current resource permissions and is scoped to the resolved site. | |
| author | query | string | Who made this version: free text, trimmed, control characters removed, at most 80 characters. Stored on the version and returned as author by GET /v1/sites/{sitename}/versions. |
Request body:
| Name | Type | Required | Description |
|---|---|---|---|
| files | object | yes | relative path -> file contents (text). index.html required unless PUT uses keep=1. |
| files_base64 | object | relative path -> base64-encoded binary contents. A path cannot also appear in files. |
|
| remove_files | array of string | Paths to drop from the live version; only with PUT keep=1. |
Responses:
| Status | Description |
|---|---|
| 201 | Site created |
| 400 | Invalid request. A saved-data write whose JSON holds a NUL character (\u0000), half of a surrogate pair or bytes that are not UTF-8 is code invalid_json. |
| 401 | Missing or invalid API key — send the X-API-Key header, not Authorization Bearer. The body’s code is missing_api_key, wrong_auth_header, invalid_api_key, key_expired or key_expired_idle (create a new key from the Keys panel). |
| 409 | Conflict: the code says which (site_exists, domain_taken, append_only, custom_domain_required, not_an_object) |
PUT /v1/sites/{sitename}/files
Same as POST, but for an existing site you own. Creates a new version and activates it; with ?publish=false the version is only stored (visitors keep seeing the live one) and the answer adds unpublished_version, preview_url and preview_expires_at. With ?create=1 a missing site is created instead (201, with the same checks as POST), so CI deploys in one call; without it a missing site is 404. With ?keep=1, send only changed files: other files in the live version are retained byte-for-byte, and remove_files drops listed relative paths. remove_files requires keep=1; index.html is not required in a patch. The merged version meets the ordinary file and storage caps.
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| publish | query | boolean | false stores the new version without making it live (updates only; a new site’s first version always goes live: 400) | |
| create | query | string, one of 1, 0, true, false |
1 creates the site when you have none of that name (reserved names, the site quota and publish=false are refused as on POST), so one PUT deploys from CI whether or not the site exists | |
| author | query | string | Who made this version: free text, trimmed, control characters removed, at most 80 characters. Stored on the version and returned as author by GET /v1/sites/{sitename}/versions. | |
| keep | query | integer, one of 1 |
Keep unchanged files from the live version; merged files use ordinary unpacked-file caps |
Request body:
| Name | Type | Required | Description |
|---|---|---|---|
| files | object | yes | relative path -> file contents (text). index.html required unless PUT uses keep=1. |
| files_base64 | object | relative path -> base64-encoded binary contents. A path cannot also appear in files. |
|
| remove_files | array of string | Paths to drop from the live version; only with PUT keep=1. |
Responses:
| Status | Description |
|---|---|
| 200 | Site updated |
| 201 | Site created (?create=1 and no site of that name) |
| 400 | Invalid request. A saved-data write whose JSON holds a NUL character (\u0000), half of a surrogate pair or bytes that are not UTF-8 is code invalid_json. |
| 401 | Missing or invalid API key — send the X-API-Key header, not Authorization Bearer. The body’s code is missing_api_key, wrong_auth_header, invalid_api_key, key_expired or key_expired_idle (create a new key from the Keys panel). |
| 404 | Not found |
POST /v1/sites/{sitename}/photos
The Add photos page at /photos prepares images on the person’s device. Upload 1 to 30 WebP or JPEG files as repeated photos multipart parts. The endpoint accepts X-API-Key, including the dashboard’s stored owner key and account-wide deploy-only keys (not site-scoped), with the normal deploy permissions and rate limit. Visitor cookies do not authorize it. Browser origins must match the apex. Default limits are 1,000,000 bytes per photo (PHOTO_MAX_BYTES) and 2048 pixels on the longest side (PHOTO_MAX_DIMENSION). The server validates headers and container structure without decoding pixels or re-encoding. This is header-only validation, not a full decode of the compressed image. JPEG allows JFIF APP0, ICC_PROFILE APP2 (16 KB total), and Adobe APP14; comments and other APP markers are refused. WebP ICCP is limited to 16 KB; EXIF, XMP, animation, unknown chunks and trailing data are refused. ICC contents are not inspected; browser canvas output carries no profile or a standard profile. Signed-in and photo-token saves share PHOTO_MAX_CONCURRENT slots (default 3), acquired before body reads. Both entry points set a body-read deadline of 60 seconds (PHOTO_BODY_TIMEOUT_SECONDS) on the real response writer; unsupported writers continue normally. Full capacity returns 429 with error “Busy, try again in a moment” and Retry-After of 2 seconds, without queueing. Filenames must match ^[a-z0-9][a-z0-9-]{0,59}.(webp|jpg)$ and the extension must match the contents. Files go under images/, with -2, -3, and further suffixes for collisions. Every other live file is kept. After an uncertain response, resend the batch with X-Photo-Reconcile: 1. This only checks for distinct live files with matching names (including collision suffixes) and identical bytes. A full match returns the current version and reconciled: true; it confirms presence, not which request wrote the files. A missing match returns 409 photo_save_uncertain. Checks never create a version. Ordinary saves create one normal published version, with normal file quotas and retention. Hosted and small-box Simple Host only.
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| Idempotency-Key | header | string | Opaque key for a create request. For 24 hours, the same authenticated account, POST path and query, key and exact request body replay the original handler status and body, including errors, without executing it again. A different body under that key returns 409 idempotency_key_reused. Concurrent identical requests wait for the first response. Stored only in this server’s memory; a restart clears it. Authentication and rate limiting still apply to replays. A replay does not extend a minted link’s expiry; use a new key to request a new link. Storage row creates support owner credentials and signed-in visitors; anonymous writes ignore this header. Storage replay also checks current resource permissions and is scoped to the resolved site. | |
| X-Photo-Reconcile | header | string, one of 1 |
Set to 1 after an uncertain save to check matching live photo bytes without writing a version. A missing match returns 409, never another upload. |
Request body: multipart/form-data
Responses:
| Status | Description |
|---|---|
| 200 | Photos added in one new live version, or matching photos confirmed without a write |
| 400 | Invalid request. A saved-data write whose JSON holds a NUL character (\u0000), half of a surrogate pair or bytes that are not UTF-8 is code invalid_json. |
| 401 | Missing or invalid API key — send the X-API-Key header, not Authorization Bearer. The body’s code is missing_api_key, wrong_auth_header, invalid_api_key, key_expired or key_expired_idle (create a new key from the Keys panel). |
| 403 | Origin or deploy permission refused |
| 404 | Site not found or feature not offered |
| 409 | A file or directory conflicts with the photo path |
| 413 | Photo |
| 429 | Deploy rate limit or photo save capacity exceeded; Retry-After advises when to retry |
POST /v1/uploads/{token}
Use the private link returned by create_upload_link with archive true from a shell. Works once for one site for 15 minutes, including failed uploads. Sign-in is checked again at upload. Links stop working when the person signs out, disconnects the app or their sign-in expires. Creates the site if missing, else publishes a new version, public at its address on success. Never share the link or put it in a web page. Usual deploy and scope checks apply.
No authentication required.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| token | path | string | yes |
Request body: application/gzip
Responses:
| Status | Description |
|---|---|
| 200 | Site updated |
| 201 | Site created |
| 400 | Invalid request. A saved-data write whose JSON holds a NUL character (\u0000), half of a surrogate pair or bytes that are not UTF-8 is code invalid_json. |
| 401 | Missing or invalid API key — send the X-API-Key header, not Authorization Bearer. The body’s code is missing_api_key, wrong_auth_header, invalid_api_key, key_expired or key_expired_idle (create a new key from the Keys panel). |
| 403 | Sign-in ended, or deploy scope or team rules refused the upload |
| 404 | Unknown, used or expired link |
| 409 | Conflict: the code says which (site_exists, domain_taken, append_only, custom_domain_required, not_an_object) |
| 413 | Payload too large (on saved data, code item_too_large; on a deploy: site_too_large (per-upload limit), site_total_too_large (whole website footprint for sites created from SITE_TOTAL_CAP_FROM), or account_storage_full (all account website files including Recently deleted). Totals count the separate live copy and kept versions after pruning; backend storage and saved data are excluded. The error names the limit and current size for total caps, and hint gives photo-compression and file-cleanup tips) |
| 429 | Rate limited (code rate_limited). Saved-data reads allow 30 a second per address with a burst of 60 (SAVED_DATA_READ_PER_SEC, SAVED_DATA_READ_BURST). |
PUT /v1/uploads/{token}
Use the private link returned by create_upload_link with archive true from a shell. Works once for one site for 15 minutes, including failed uploads. Sign-in is checked again at upload. Links stop working when the person signs out, disconnects the app or their sign-in expires. Creates the site if missing, else publishes a new version, public at its address on success. Never share the link or put it in a web page. Usual deploy and scope checks apply.
No authentication required.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| token | path | string | yes |
Request body: application/gzip
Responses:
| Status | Description |
|---|---|
| 200 | Site updated |
| 201 | Site created |
| 400 | Invalid request. A saved-data write whose JSON holds a NUL character (\u0000), half of a surrogate pair or bytes that are not UTF-8 is code invalid_json. |
| 401 | Missing or invalid API key — send the X-API-Key header, not Authorization Bearer. The body’s code is missing_api_key, wrong_auth_header, invalid_api_key, key_expired or key_expired_idle (create a new key from the Keys panel). |
| 403 | Sign-in ended, or deploy scope or team rules refused the upload |
| 404 | Unknown, used or expired link |
| 409 | Conflict: the code says which (site_exists, domain_taken, append_only, custom_domain_required, not_an_object) |
| 413 | Payload too large (on saved data, code item_too_large; on a deploy: site_too_large (per-upload limit), site_total_too_large (whole website footprint for sites created from SITE_TOTAL_CAP_FROM), or account_storage_full (all account website files including Recently deleted). Totals count the separate live copy and kept versions after pruning; backend storage and saved data are excluded. The error names the limit and current size for total caps, and hint gives photo-compression and file-cleanup tips) |
| 429 | Rate limited (code rate_limited). Saved-data reads allow 30 a second per address with a burst of 60 (SAVED_DATA_READ_PER_SEC, SAVED_DATA_READ_BURST). |