Skip to content
Dashboard
Menu Deploying · Deploy a drop

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)

Raw HTML JSON files map Zip

Request

bash
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

json 200 OK
{
"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.

bash
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:

bash
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