Deploying
Deploy a drop
Every drop comes from one endpoint. Send the body as raw HTML, a
JSON map of files, or a zip — ship.page picks the format from the
Content-Type
header and answers with the same JSON envelope either way.
POST
https://ship.page/deploy
Creates a drop from the request body and returns its live URL.
Auth:
none
(optional
sp_
key or Firebase ID token)
Request
curl -X POST https://ship.page/deploy \
-H "Content-Type: text/html" \
--data-binary @index.html
A
text/html
body becomes
index.html
at the root of the drop.
Response
{
"slug": "vast-juice-c2dse",
"url": "https://vast-juice-c2dse.shipped.run/",
"files": ["index.html"],
"plan": "free",
"password_protected": false,
"expires_at": "2026-06-17T18:00:00.000Z",
"claim_token": "spc_7xk3m9q2vj8n4p1w6z5y0r2t"
}
The envelope is identical for all three body formats —
url
is live the moment the response lands.
claim_token
only appears on anonymous deploys and is shown exactly once — anyone holding it can attach the drop to an account via
POST /drops/<slug>/claim. Authenticated deploys are owned already and never mint one.
Errors
| Status and error code | Description |
|---|---|
400
bad request
|
The body didn’t parse or breaks a rule — malformed JSON files map, an invalid zip, a bad path, an out-of-range ttl, or more files than your plan’s per-request cap.
|
401
unauthorized
|
?name= without a credential — named drops and password protection need an API key. Plain anonymous deploys never see this.
|
402
payment required
|
?name= or password protection with a valid key but no active subscription — a ttl past your plan’s cap is a 400 instead. See Limits & plans.
|
413
payload too large
|
The zip exceeds your plan’s cap (25 MB free, 100 MB subscribed) or decompresses past 4× that — or a non-zip body tops 10 MB on any plan. |
429
rate limited
|
Too many deploys in a short window — 10/min per IP anonymous and free, 120/min per account on paid plans. Every 429 carries Retry-After: 60 — back off and retry. |
Every error answers with { "error": "<message>", "code": "<stable-code>" } — the full catalogue lives in Errors.
JSON files map
For multi-file drops without an archive, send
Content-Type: application/json
with a
files
object. Text files go in as plain strings; binary assets use an
object with base64 encoding. Each key becomes a path on the drop.
curl -X POST https://ship.page/deploy \
-H "Content-Type: application/json" \
-d '{
"files": {
"index.html": "<!doctype html><h1>It works</h1>",
"styles.css": "h1 { color: #06b6d4; }",
"logo.png": { "encoding": "base64", "content": "iVBORw0KGgoAAAANSUhEUgAA…" }
}
}'
Zip upload
When a build tool already produced a folder, zip it and stream the
archive with
Content-Type: application/zip:
curl -X POST https://ship.page/deploy \
-H "Content-Type: application/zip" \
--data-binary @dist.zip
If the archive wraps everything in a single root directory (say
dist/), it's stripped automatically — your
index.html
still ends up at
/.
Password protection (Pro / Team)
Send X-Ship-Password with encodeURIComponent(password) and your Bearer credential to protect a new drop or rotate a named drop’s password. Use 8–128 Unicode characters, at most 512 UTF-8 bytes. The header works with all three body formats; do not put the password in the URL or JSON files map.
await fetch("https://ship.page/deploy", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "text/html",
"X-Ship-Password": encodeURIComponent(password),
},
body: html,
});
Setting or rotating a password requires an active paid Pro/Team subscription: missing auth returns 401, an ineligible plan 402, and unavailable billing 503. An empty password is invalid, not a request to remove protection. The response includes password_protected; the password is never returned. Omitting the header on a live named redeploy preserves existing protection, as does appending files. Recreating an expired or purged named drop is a new deploy: send the password again. A top-level JSON password field is rejected, and append rejects password input; use the owner endpoints to rotate it. The /_ship/ namespace is reserved and cannot be uploaded.
Visitors see a 401 password gate until they unlock on the drop’s own origin; the host-only session lasts 12 hours. All protected files bypass caching. Changing a password, locking a formerly public drop, or reuploading content cannot recall copies already downloaded or cached. See password management and browser unlock.
Last updated Jun 12, 2026