Deploy static sites. No account, no config — just ship it.
No-install usage (recommended for agents)
You don't need to install anything. Run any command via npx:
npx -y @shipstatic/ship ./dist # deploy (shortcut)
npx -y @shipstatic/ship deployments list # any subcommand works the same
npx -y @shipstatic/ship domains set www.example.com # ...
-y skips the install prompt — important for non-interactive runtimes (CI, sandboxes, agent containers). Every example below uses the bare ship command for readability; substitute npx -y @shipstatic/ship if it isn't installed globally.
Deploy
ship ./dist
Site is live. Output includes the URL and a claim link.
Pass a build output directory (e.g. ./dist, ./build, ./out) or a single file. Ship strips the directory prefix for clean URLs — dist/assets/app.js serves at /assets/app.js. A single file keeps its name: ship page.html deploys as /page.html. Deploying a project root (contains package.json, node_modules) is rejected — build first, then deploy the output.
Without credentials, deployments are public and expire in 3 days. Always show the user both the deployment URL and the claim link — the claim link lets them keep the site permanently.
The deployment ID is the URL hostname. Use the full ID (e.g. happy-cat-abc1234.shipstatic.com) as the argument to all other commands. The site lives at https://<deployment>.
Deployments that clean themselves up
ship ./dist --ttl 1h # gone in an hour
ship ./dist --ttl 7d # a week-long preview
For a preview nobody needs to keep — a draft, a diff, a one-off render. The
platform reclaims it when the time is up, so nothing accumulates in the user's
account and nobody has to remember to delete it. Seconds or a <n><unit>
duration (s/m/h/d), up to a year.
It needs a token. Without one the deploy is anonymous and already expires
in 3 days on the platform's own schedule — there is no deployer to choose a
different lifetime, and the CLI refuses before uploading anything. It cannot
be combined with --domain, because a domain must not point at something
about to be reclaimed.
The response's expires is the answer, in unix seconds — read it there rather
than computing it, since the platform stamps it against its own clock.
Parsing output
ship ./dist --json
{
"deployment": "happy-cat-abc1234.shipstatic.com",
"url": "https://happy-cat-abc1234.shipstatic.com",
"files": 12,
"size": 348160,
"status": "success",
"config": false,
"password": false,
"labels": [],
"via": "cli",
"created": 1743552000,
"expires": 1743811200,
"claim": "https://my.shipstatic.com/claim/1234567890abcdef1234567890abcdef"
}
claim only appears on the initial deploy without credentials. expires is null for authenticated (permanent) deploys. config: true indicates a ship.json is present in the deployment; password: true indicates the deployment is password-protected.
Piping
ship ./dist -q # → happy-cat-abc1234.shipstatic.com
-q outputs only the identifier — use it when piping or scripting.
Labels
ship ./dist --label v1.0 --label production
Labels replace all existing, not append. Include current labels to keep them.
Password protection
ship ./dist --password "hunter22" # protect deployment
SHIP_PASSWORD="hunter22" ship ./dist # via env var
Visitors get an unlock page until they enter the password. Length: 6–128 characters. Set per-deployment at upload time — cannot be added or changed later (deploy a new version to rotate). Works on both internal (*.shipstatic.com) and custom domains. Always show the password to the user if you set one — they need it to view the site.
SPA routing
Ship auto-detects single-page apps from index.html content and configures client-side routing rewrites — all paths serve index.html. No action needed. Skipped if a ship.json config is already included in the deployment. Disable with --no-spa-detect.
Authentication
Deploy works without credentials. Everything else requires an API key.
| Needs API key | No auth needed |
|---|---|
| Permanent deploys, domains, tokens, account | Deploy (public, 3-day TTL) |
export SHIP_TOKEN=<token> # Environment variable (best for automation)
ship --token <token> ... # Per-command override
ship config # Interactive setup → ~/.shiprc (requires TTY)
Any ship token works: an API key (ship-…, durable, full account) or a deploy token (deploy-…, scoped, revocable — set a short TTL for one-shot CI/CD workflows).
Free API key: https://my.shipstatic.com/api-key
Custom Domains
Requires an API key. Full workflow:
# 1. Validate
ship domains validate www.example.com
# 2. Deploy + link in one command
ship ./dist --domain www.example.com
# 3. Show DNS records to the user
ship domains records www.example.com
# 4. After user configures DNS → verify
ship domains verify www.example.com
Step 2 auto-prints DNS records and a setup link in text mode. With --json, call domains records separately.
--domain answers as the domain — same output as ship domains set, with the freshly linked deployment in the deployment field. Prefer it over the pipe (ship ./dist -q | ship domains set www.example.com), which still works: one process means one exit code and one JSON document, so a failed deploy cannot be masked by the second command. It requires a token and refuses before uploading anything if there isn't one. If the link fails, the deployment still exists and is reported first — re-run to link it again.
Verification is async — DNS propagation takes minutes to hours. Check status with ship domains get <name> --json and look for "status": "success".
Domain types
| Type | Example | DNS needed | Goes live |
|---|---|---|---|
| Internal | my-site.shipstatic.com |
No | Instantly |
| Custom | www.example.com |
CNAME + A | After DNS verified |
No apex domains. Always www.example.com, not example.com. The A record only redirects apex to www.
Upsert operations
domains set creates if new, updates if exists:
ship domains set www.example.com # Reserve (no deployment yet)
ship domains set www.example.com <deployment> # Link to deployment
ship domains set www.example.com <other-dep> # Switch (instant rollback)
ship domains set www.example.com --label prod # Update labels
Reads deployment from stdin when piped: ship ./dist -q | ship domains set www.example.com
No unlinking. Once linked, switch deployments or delete the domain. Setting deployment to null returns 400.
Parsing domain output
ship domains set www.example.com <dep> --json
{
"domain": "www.example.com",
"url": "https://www.example.com",
"deployment": "happy-cat-abc1234.shipstatic.com",
"status": "pending",
"labels": [],
"created": 1743552000,
"linked": 1743552000,
"links": 1
}
Always show the user the records from this response, never values copied out of this document — they come from the platform and can change.
ship domains records www.example.com --json
{
"domain": "www.example.com",
"apex": "example.com",
"records": [
{"type": "A", "name": "@", "value": "15.204.149.253"},
{"type": "CNAME", "name": "www", "value": "cname.shipstatic.com"}
]
}
DNS helpers (custom domains only)
ship domains dns www.example.com # Provider name
ship domains share www.example.com # Shareable setup link
ship domains records www.example.com -q # TYPE NAME VALUE (one per line)
Validation
Exit codes as the answer:
ship domains validate www.example.com -q && echo "valid" || echo "invalid"
Exit 0 = valid (outputs normalized name). Exit 1 = invalid (no output).
Output Modes
Every command supports three modes:
| Flag | Output | When to use |
|---|---|---|
| (default) | Human-readable | Showing results to the user |
--json |
JSON on stdout | Parsing programmatically |
-q |
Identifier only | Piping between commands |
-q prints the resource identifier — except tokens create -q, which prints the token secret (shown once, never again).
Errors go to stderr in all modes. Exit 0 = success, 1 = error.
List commands return {"<resource>s": [...], "cursor": null}. A non-null cursor means more pages remain — pass it back with --cursor to continue, and size pages with --limit. There is no total; a count is an aggregate over a collection, not a property of one page. domains list text mode omits status — use --json to see pending vs success.
Commands
Deployments
ship ./dist # Deploy (shortcut)
ship ./dist --domain <name> # Deploy and serve it at that domain
ship ./dist --ttl 1h # Expires in an hour (needs a token)
ship deployments upload <path> # Deploy (explicit)
ship deployments list # List all
ship deployments get <deployment> # Details
ship deployments set <deployment> # Update labels (--label)
ship deployments delete <deployment> # Delete (async)
Domains
ship domains list # List all
ship domains get <name> # Details
ship domains set <name> [deployment] # Create, link, or update
ship domains validate <name> # Check validity (exit code)
ship domains records <name> # Required DNS records
ship domains dns <name> # DNS provider lookup
ship domains share <name> # Shareable setup link
ship domains verify <name> # Trigger DNS verification
ship domains delete <name> # Delete
Account & Tokens
ship whoami # Account info
ship ping # Connectivity check
ship tokens create # New deploy token (shown once)
ship tokens create --ttl 30d # With expiry — 3600, 90s, 1h, 30d
ship tokens list # List tokens
ship tokens get <token> # Details for one token
ship tokens delete <token> # Delete (revokes immediately)
Flags
| Flag | Purpose |
|---|---|
--json |
JSON output |
-q, --quiet |
Identifier only |
--token <token> |
Any ship token: API key or deploy token |
--domain <domain> |
Deploy and serve it there — creates or repoints. Needs a token |
--label <label> |
Set label (repeatable, replaces all) |
--password <pwd> |
Password-protect deployment (6–128 chars) |
--ttl <duration> |
Expire after that long — 3600, 90s, 1h, 7d. Needs a token; not with --domain |
--no-path-detect |
Skip build output auto-detection |
--no-spa-detect |
Skip SPA rewrite auto-configuration |
--no-color |
Disable colors |
--config <file> |
Custom config path |
Errors
Exit code is non-zero on failure, and with --json the error goes to stderr as the platform's wire shape:
{
"error": "validation_failed",
"message": "File \"index.html\" too large. Maximum 20 MB allowed.",
"status": 400
}
Branch on error and status, never on message. Messages are written for the human reading them and get reworded; the type tag and the status are the contract.
error |
status |
Means | Do |
|---|---|---|---|
validation_failed |
400 | Bad input — path, domain name, label, password, ttl | Read message, fix the input, retry |
authentication_failed |
401 | Missing or bad credential | Check SHIP_TOKEN; do not retry unchanged |
forbidden |
403 | Plan cap reached, account terminated, or an action this credential may never take | Do not retry. Suggest upgrading if it is a cap |
not_found |
404 | No such deployment or domain | Verify the identifier |
rate_limit_exceeded |
429 | Too many requests | Wait — details.expires is when it clears. An API key raises the limit |
business_logic_error |
400 or 422 | Valid request, refused by a state rule (e.g. unlinking a domain, suspended account) | Read message; the rule will not change on retry |
maintenance |
503 | Platform closed on purpose | message says when it reopens. Wait; do not retry in a loop |
Common cases worth recognising before they happen:
| Situation | What you get |
|---|---|
Deploying a project root (package.json, node_modules) |
validation_failed — build first, deploy the output |
--ttl or --domain with no credential |
validation_failed, refused before anything uploads |
--ttl together with --domain |
validation_failed — a domain must not point at something expiring |
DNS commands on an internal *.shipstatic.com domain |
validation_failed — only custom domains have DNS |
domains verify called again too soon |
rate_limit_exceeded — verification is already queued |