ShipStatic Docs llms.txt llms-full.txt

Publish with no credentials at all, or authenticate for everything else. This is the API behind every other interface — anything the Web, CLI and SDK can do, you can do directly.

https://api.shipstatic.com

Deploy to your account

Deploying needs no account — POST /deployments with no Authorization header works, and the response carries a claim URL the user can visit to keep the site permanently. Everything else needs a credential: pass an API Key or a deploy token in the Authorization header. The prefix says which it is:

Method Format Use case
API key Bearer ship-{32 hex} Persistent access, full account
Deploy token Bearer deploy-{32 hex} Scoped to deploys, optional TTL

One slot takes either — the prefix is what tells the two apart, so there is no precedence to reason about.

Endpoints

Deployments

Method Path Description
POST /deployments Upload a new deployment (multipart)
POST /deployments/:deployment/files Send more files to a deployment declared with a manifest (see Large deploys)
GET /deployments List all deployments (paginated)
GET /deployments/:deployment Get deployment details
PATCH /deployments/:deployment Update labels
DELETE /deployments/:deployment Delete deployment (async)

POST /deployments accepts an optional password field (6–128 characters) to protect the deployment with an unlock page. Passwords are handled securely server-side and can never be read back through the API. Deployment responses expose password: boolean indicating whether protection is active.

Domains

Method Path Description
PUT /domains/:domain Create, link, switch, or label (upsert)
GET /domains List all domains (paginated)
GET /domains/:domain Get domain details
DELETE /domains/:domain Delete domain
POST /domains/validate Pre-flight availability check (name in the body)
POST /domains/:domain/verify Trigger DNS verification
GET /domains/:domain/dns Which DNS provider hosts the domain (Cloudflare, Namecheap, …)
GET /domains/:domain/records The DNS records you need to add
GET /domains/:domain/share Shareable DNS setup link

Tokens

Method Path Description
POST /tokens Create token (full secret returned only here)
GET /tokens List all tokens (short identifiers only)
DELETE /tokens/:token Revoke token

Account

Method Path Description
GET /account Get current account
POST /account/claim Claim a public deployment with a claim hash
GET /activities Account audit log (paginated)
GET /labels Distinct labels in use across the account
GET /limits Plan-based platform limits (max file size, file count, total size)
GET /ping Health check

Plans

Method Path Description
GET /plans The plan menu: every plan on sale, with its price and caps. Public, no credential. Its fields are described under Plans

Examples

Anonymous public deploy

curl -X POST https://api.shipstatic.com/deployments \
  -F 'files[]=@dist/index.html' \
  -F 'files[]=@dist/styles.css'

Response (201):

{
  "deployment": "happy-cat-abc1234",
  "url": "https://happy-cat-abc1234.shipstatic.com",
  "claim": "https://my.shipstatic.com/claim/...",
  "files": 2,
  "size": 4321,
  "status": "success",
  "password": false,
  "via": "sdk",
  "created": "...",
  "expires": "..."
}

claim and expires are present only on anonymous deploys. Visiting claim while signed in transfers the deployment to your account.

Authenticated deploy

curl -X POST https://api.shipstatic.com/deployments \
  -H 'Authorization: Bearer ship-your-api-key' \
  -F 'files[]=@dist/index.html' \
  -F 'labels=["production"]' \
  -F 'password=hunter22'

Response is the same shape minus claim and expires.

List with pagination

List endpoints return a cursor, which is null on the last page. Pass a non-null one back as ?cursor= to fetch the next page.

curl -H 'Authorization: Bearer ship-your-api-key' \
  'https://api.shipstatic.com/deployments?limit=50'
{
  "deployments": [...],
  "cursor": "eyJpZCI6ImhhcHB5LWNhdC1hYmMxMjM0In0"
}

Custom domain — full flow

# 1. Validate the name
curl -X POST -H 'Authorization: Bearer ship-your-api-key' \
  https://api.shipstatic.com/domains/www.example.com/validate

# 2. Create + link to a deployment
curl -X PUT -H 'Authorization: Bearer ship-your-api-key' \
  -H 'Content-Type: application/json' \
  -d '{"deployment":"happy-cat-abc1234"}' \
  https://api.shipstatic.com/domains/www.example.com

# 3. Get required DNS records
curl -H 'Authorization: Bearer ship-your-api-key' \
  https://api.shipstatic.com/domains/www.example.com/records

# 4. After configuring DNS, trigger verification
curl -X POST -H 'Authorization: Bearer ship-your-api-key' \
  https://api.shipstatic.com/domains/www.example.com/verify

Claim a public deployment

curl -X POST https://api.shipstatic.com/account/claim \
  -H 'Authorization: Bearer ship-your-api-key' \
  -H 'Content-Type: application/json' \
  -d '{"claim":"<claim-hash-from-deploy-response>"}'

Returns the claimed deployment, now bound to your account.

Multipart upload shape (POST /deployments)

Field Type Notes
files[] File parts One per file. The filename carries the path (webkitRelativePath style)
manifest JSON-encoded array of { path, size, md5 } Optional. Lists every file of a deployment sent in several requests (see Large deploys). Without it, the files you send are the whole deployment
labels JSON-encoded string[] Optional
password string Optional, 6–128 characters
ttl number of seconds Optional, needs a credential. The deployment is deleted when the time is up
via string Optional origin tag (cli, sdk, mcp, etc.)
spa true or false Optional. Send true and we check whether the site is a single-page app: if its index.html is an empty app shell and you sent no ship.json, we add the routing config that serves every path from index.html. Leave it out or send false and we add nothing. The CLI, the SDK and the integrations send true for you

The API works out each file's MD5 from what it receives, so a deploy in one request needs no manifest.

Every deployment needs an index.html at the top, the page visitors see first. A deployment without one, or with an empty one, is refused with 400 validation_failed, before anything is stored. An empty file is never stored, so any other empty file is left out rather than refused. Each path can appear only once: index.html and /index.html are the same file.

Large deploys

One request can carry at most 64 MB. The CLI and the SDK split a bigger site for you, and so does every tool built on them, so you only need this if you write your own client.

A bigger site goes in several requests:

  1. Declare it. Send POST /deployments with a manifest field listing every file: its path, its size in bytes and its MD5. If the manifest lists a root ship.json, send it in this request, because it sets up the site. If you send spa=true, send the root index.html here too, because it's the page we check. You can send other files here too. If every file is there, the answer is 201 and the site is live. If not, the answer is 202, and the deployment's status is pending.
  2. Send the rest. Send the other files to POST /deployments/:deployment/files as files[], in as many requests as you need. Each answer is 202 while files are still missing, and 201 when the last one arrives and the site goes live.

A few things to know:

  • Each file must match the manifest: the same path, size and MD5. A file that doesn't is refused.
  • Sending a file again is safe. It replaces itself with the same bytes. Once the site is live, the files route changes nothing and answers 200 with the deployment.
  • It needs a credential. A deploy without an account always fits in one request, so it never needs the files route.
  • If you stop partway, nothing is published. We delete the unfinished deployment after a while, and the files route then answers 404. Start again with a new declaration.

Status Codes

Code Meaning Example
200 Success Read, update, synchronous delete
201 Created POST /deployments, POST /tokens
202 Accepted DELETE /deployments/:deployment (async cleanup), or a large deploy with files still to come
400 Bad request Invalid input, missing fields, or a request the platform refuses
401 Authentication error Missing or invalid credentials
403 Forbidden Account terminated, plan limit reached, operation not permitted
404 Not found Resource doesn't exist or belongs to another account
409 Conflict A domain already taken, a domain operation already in progress, a deployment a domain still points at
413 Payload too large A deploy request over 64 MB, or a JSON body over its own, smaller limit (the message says how big)
415 Unsupported media type A deploy sent as neither multipart/form-data nor application/json
422 Unprocessable The project failed to build, or the resource's state doesn't allow this operation
429 Rate limited Too many requests; retry after Retry-After seconds
500 Server error Something failed on our side
503 Unavailable The platform is in maintenance, or a service it depends on is briefly unavailable

Errors

Every error answers the same shape:

{
  "error": "not_found",
  "message": "Deployment not found",
  "status": 404
}

error is the type tag, and one of these:

Tag Status Meaning
validation_failed 400 The input is malformed
authentication_failed 401 Credentials are missing or invalid
forbidden 403 Authenticated, but not allowed to do this
not_found 404 The resource doesn't exist, or isn't yours
rate_limit_exceeded 429 Too many requests; wait Retry-After seconds
business_logic_error 400, 409, 413, 415, 422 Well formed, but the platform refuses it; the message says why
build_failed 422 The project could not be built. details.log carries the tail of the build output. Fix the project; retrying gives the same answer
maintenance 503 The platform is closed on purpose. The message says when it reopens, so wait rather than retrying in a loop
internal_server_error 500, 503 Something failed on our side; a later retry may succeed

message is written for the person using your tool, so show it as it is. Some errors add a details object with structured context.

Branch on error and status, never on the message text. Messages are rewritten for clarity; the tag and the status are the contract.

Pagination

List endpoints (/deployments, /domains, /tokens, /activities) return cursor-paginated results:

{ "deployments": [...], "cursor": "..." }

Pass the cursor as ?cursor=... to fetch the next page. cursor is always present, and null on the final page. Default page size is 50; pass ?limit=N to override (capped at 300).

Rate Limits

Per-account global limiter; deploy endpoints additionally limited per-credential to prevent abuse. On exhaustion, the API returns 429 with a Retry-After header (seconds). Anonymous public deploys have a tighter per-IP limit than authenticated requests.

Idempotent Deploys

POST /deployments accepts an optional Idempotency-Key header (up to 256 characters; a UUID is a good choice). The key names the deployment the request creates. Send the same key again within 24 hours and you get that deployment back as it is now, with 201 and Idempotency-Replay: true, instead of a second one. A key works only within the account that sent it. Without an account, the key is ignored and each request creates a new deployment. Use it whenever your client retries on timeout. Only a deployment that went live is answered this way, so retrying after an error deploys again. For a large deploy, send the key on the declaration. The files route takes none, because sending a file again is already safe.

Conventions

Content type. All requests and responses use application/json. Deployment upload also accepts multipart/form-data.

Domain names in paths are URL-encoded. The API normalizes casing and Unicode (e.g. Example.COM → example.com).

Async operations. Only deployment deletion is asynchronous (returns 202). All other deletes are synchronous (200).

Errors share one shape and one set of tags; see Errors.