Deploys
A deploy is a single PUT to the project's domain, with the project packed as a gzipped tarball in the request body. The response is a live stream of newline-delimited JSON describing the publish as it happens: upload, propagation to the CDN, certificate work, and the final result.
PUT /:domain
Request
PUT /my-project.surge.sh HTTP/1.1
Host: surge.surge.sh
Authorization: Basic dG9rZW46…
Content-Type: application/gzip
Accept: application/x-ndjson
Transfer-Encoding: chunked
version: 1.0.0
file-count: 42
project-size: 123456
The body is a gzipped tar archive of the project directory. Notes on the format the official clients produce:
- Entries are prefixed with a single top-level directory (the server unpacks with the first path segment stripped), so
project/index.htmlbecomes/index.html. - Files matched by the default ignore rules (
.git, dotfiles,*.*~backups,node_modules,bower_components) and by the project's.surgeignoreare excluded before packing. - A single
.htmlfile can be published by packing it alone asindex.html. - The official clients first ask
POST /:domain/carrywhich files the live revision already has, and pack only the rest (thecarry-ticketheader ties the two requests together). It's an optimization; a full tarball always works.
Deploy headers
Metadata about the deploy travels in request headers:
| Header | Description |
|---|---|
version |
Your client's version string. |
file-count |
Number of files in the archive—used for progress totals and limits. |
project-size |
Total bytes—used for progress and limits. |
message |
A deploy message, shown in the revision list (surge <domain> revs). |
stage |
Set to make this a preview deploy: uploaded and given a revision preview URL, but not cut over to production. |
add / rem |
Comma-separated emails. rem removes collaborators invited before orgs; add invites no one now and the info line says so. |
argv |
JSON of the client's parsed arguments. Domain settings ride here and are applied on publish—{ "force": "https" }, { "private": true }—the same keys as PUT /:domain/settings. |
The response stream
The server responds 200 and begins streaming JSON objects, one per line. Each object has a type. In a typical successful deploy you'll see, in order:
{ "type": "progress", "id": "upload", "written": 51200, "total": 123456 }
{ "type": "progress", "id": "cdn", "written": 12, "total": 42, "file": "index.html" }
{ "type": "cert", "data": { "issuer": "Let's Encrypt", "altnames": ["my-project.surge.sh"], "expiresInWords": "89 days" } }
{ "type": "subscription", "data": null }
{ "type": "info", "urls": [...], "instances": [...], "metadata": {...}, "config": {...} }
| Type | Meaning |
|---|---|
progress |
Progress through a phase. id is upload, cdn, or encrypt; the final message of a phase carries "end": true. |
cert |
Certificate details, when certificate work happens during the deploy. |
subscription |
The project's plan, if any. |
collect |
Payment required—carries plan, perks, stripe_pk, and a checkout_url. The deploy will not complete until the account subscribes. |
msg |
A human-readable server message: { payload: { msg, status } }. |
error |
The deploy was refused mid-stream: { error: { message } }—a project over the size limits, an AUTH file without Pro, a CDN failure. Nothing was published. |
info |
The success marker. Contains the deploy result: urls (where the project is reachable, including the revision preview URL), instances (the edge nodes serving it), metadata (revision id, message, file counts, sizes; metadata.preview is the preview host), and cta (one line the server wants shown, when it has one). |
A stream that ends without an info line means the deploy did not succeed. Note that a refusal can arrive inside a 200 stream as an error line—the status code alone doesn't mean success.
Failures
Failures before the stream starts arrive as plain HTTP errors:
| Status | Reason |
|---|---|
401 |
Invalid or expired token. The explanation is in the JSON body. |
403 |
No permission to publish to this domain (the reason header says why when it can), a token scoped elsewhere, or a project whose declared file-count/project-size exceed the limits (10,100 files / 450 MB; reason: Unable to publish. Application too large). A project that only goes over during the upload is stopped with an in-stream error line instead. |
422 |
The domain is invalid. |
429 |
Rate limited. Per 15 minutes: 30 deploys per domain, 60 per account, 240 per IP. Per 24 hours: 200 per domain, 500 per account. Halved for unverified accounts. Body { "message": "…" }. The retry-after header gives the seconds until the window allows another deploy; rejected attempts count toward the window, so wait it out rather than retrying. See publishing limits. |
Revisions and previews
Every deploy creates a revision, identified by a millisecond timestamp, and each revision gets its own preview URL—<rev>-example.surge.sh for a surge.sh subdomain, <rev>--example-com.surge.sh for a custom domain. Surge keeps the last 100 revisions per domain. A normal deploy is cut over to production immediately; a deploy with the stage header stops after upload, leaving production untouched until you cut it over.
Revisions are managed with a small family of endpoints—list, rollback, rollfore, cutover, discard—documented in the Endpoint Reference, and correspond directly to the CLI's revision commands.
Certificates over the same stream
Two more endpoints speak the same NDJSON protocol:
PUT /:domain/encrypt— provision a managed SSL certificate for a custom domain. No body. Streamsprogress(idencrypt) and always ends withinfo, whosecertslists the domain's valid certificates—an empty list means none was issued.PUT /:domain/ssl— upload a custom PEM bundle (Content-Type: application/x-pem-file, raw body). Requires Pro. The response is always200and carries noinfoline; the outcome is the finalmsgline:{ "type": "msg", "payload": { "msg": "the pem file has been applied", "status": "ok" } }, or"status": "error"when the bundle is rejected.
This page as markdown · every page in llms-full.txt