Skip to content

API reference

Deploy a drop

One endpoint for your HTML, files, or static build. Send what you made. Get a live URL back.

On this page
POSThttps://ship.page/deployAuth optional

Request body

The Content-Type header selects the format. All three return the same response envelope.

HTML

Send the file itself as the body. It becomes index.html at the root of your drop.

bash
curl -X POST https://ship.page/deploy \
  -H "Content-Type: text/html" \
  --data-binary @index.html

Content-Type text/html

JSON files map

Each key in files becomes a path. Text files are plain strings; binary assets use an object with base64 encoding. Body formats covers binary assets.

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: #264fc5; }"
    }
  }'

Content-Type application/json

Zip upload

Send a ZIP of your static build. A single enclosing folder, such as dist/, is stripped automatically — your index.html still ends up at /.

bash
curl -X POST https://ship.page/deploy \
  -H "Content-Type: application/zip" \
  --data-binary @dist.zip

Content-Type application/zip

Headers

Content-TypeBody format
text/html, application/json, or application/zip.
AuthorizationOptional
Use Bearer with an sp_ API key or Firebase ID token to attach the drop to your account. Authentication guide
X-Ship-PasswordPro / Team
Send encodeURIComponent(password) with your Bearer credential. Passwords must be 8–128 Unicode characters, at most 512 UTF-8 bytes. Don’t put the password in the URL or files map.
Password protection details

Omitting the header on a live named redeploy preserves its password. An empty value is invalid; it does not remove protection. When recreating an expired or purged named drop, send the password again.

The password is never returned. See the full password protection reference below, or password management to rotate or remove it later.

For a shorter lifetime or stable alias, see query parameters: ttl and name.

Response

A successful request returns 200 OK and a JSON body. The URL is live when the response arrives.

json200 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"
}
urlstring
The shareable URL, on the drop’s own subdomain.
slug / filesstring / string[]
The drop identifier and the file paths deployed.
planstring
The plan used for this deployment.
password_protectedboolean
Whether the drop requires a password to view.
expires_attimestamp | null
The expiry time, or null for a drop without expiry.
claim_tokenAnonymous only
Returned once. Anyone holding it can attach the drop to an account via POST /drops/<slug>/claim. Authenticated deploys are already owned and don’t return one.

Errors & retries

Errors return an error message and a stable code. Use the HTTP status to decide what to do next.

400Bad 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.
401Unauthorized
?name= without a credential — named drops and password protection need an API key. Plain anonymous deploys never see this.
402Payment 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.
413Payload 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.
429Rate 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.

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