# API

> The REST API behind every interface: authentication, endpoints, upload formats, status codes, error tags, pagination, rate limits and idempotent deploys.

Source: https://docs.shipstatic.com/api

Publish with no credentials at all, or authenticate for everything else. This is the API behind every other interface — anything the Web, CLI and SDK can do, you can do directly.

```
https://api.shipstatic.com
```

## Deploy to your account

**Deploying needs no account** — `POST /deployments` with no `Authorization` header works, and the response carries a `claim` URL the user can visit to keep the site permanently. Everything else needs a credential: pass an [API Key](https://docs.shipstatic.com/api-key) or a [deploy token](https://docs.shipstatic.com/tokens) in the `Authorization` header. The prefix says which it is:

| Method | Format | Use case |
|--------|--------|----------|
| API key | `Bearer ship-{32 hex}` | Persistent access, full account |
| Deploy token | `Bearer deploy-{32 hex}` | Scoped to deploys, optional TTL |

One slot takes either — the prefix is what tells the two apart, so there is no precedence to reason about.

## Endpoints

### Deployments

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/deployments` | Upload a new deployment (multipart) |
| `GET` | `/deployments` | List all deployments (paginated) |
| `GET` | `/deployments/:deployment` | Get deployment details |
| `PATCH` | `/deployments/:deployment` | Update labels |
| `DELETE` | `/deployments/:deployment` | Delete deployment (async) |

`POST /deployments` accepts an optional `password` field (6–128 characters) to protect the deployment with an unlock page. Passwords are handled securely server-side and can never be read back through the API. Deployment responses expose `password: boolean` indicating whether protection is active.

### Domains

| Method | Path | Description |
|--------|------|-------------|
| `PUT` | `/domains/:domain` | Create, link, switch, or label (upsert) |
| `GET` | `/domains` | List all domains (paginated) |
| `GET` | `/domains/:domain` | Get domain details |
| `DELETE` | `/domains/:domain` | Delete domain |
| `POST` | `/domains/validate` | Pre-flight availability check (name in the body) |
| `POST` | `/domains/:domain/verify` | Trigger DNS verification |
| `GET` | `/domains/:domain/dns` | Which DNS provider hosts the domain (Cloudflare, Namecheap, …) |
| `GET` | `/domains/:domain/records` | The DNS records you need to add |
| `GET` | `/domains/:domain/share` | Shareable DNS setup link |

### Tokens

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/tokens` | Create token (full secret returned only here) |
| `GET` | `/tokens` | List all tokens (short identifiers only) |
| `DELETE` | `/tokens/:token` | Revoke token |

### Account

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/account` | Get current account |
| `POST` | `/account/claim` | Claim a public deployment with a claim hash |
| `GET` | `/activities` | Account audit log (paginated) |
| `GET` | `/labels` | Distinct labels in use across the account |
| `GET` | `/limits` | Plan-based platform limits (max file size, file count, total size) |
| `GET` | `/ping` | Health check |

### Plans

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/plans` | The plan menu: every plan on sale, with its price and caps. Public, no credential. Its fields are described under [Plans](https://docs.shipstatic.com/plans#prices-and-caps) |

## Examples

### Anonymous public deploy

```bash
curl -X POST https://api.shipstatic.com/deployments \
  -F 'files[]=@dist/index.html' \
  -F 'files[]=@dist/styles.css' \
  -F 'checksums=["d41d8cd98f00b204e9800998ecf8427e","..."]'
```

Response (201):

```json
{
  "deployment": "happy-cat-abc1234",
  "url": "https://happy-cat-abc1234.shipstatic.com",
  "claim": "https://my.shipstatic.com/claim/...",
  "files": 2,
  "size": 4321,
  "status": "success",
  "password": false,
  "via": "sdk",
  "created": "...",
  "expires": "..."
}
```

`claim` and `expires` are present only on anonymous deploys. Visiting `claim` while signed in transfers the deployment to your account.

### Authenticated deploy

```bash
curl -X POST https://api.shipstatic.com/deployments \
  -H 'Authorization: Bearer ship-your-api-key' \
  -F 'files[]=@dist/index.html' \
  -F 'checksums=["d41d8cd98f00b204e9800998ecf8427e"]' \
  -F 'labels=["production"]' \
  -F 'password=hunter22'
```

Response is the same shape minus `claim` and `expires`.

### List with pagination

List endpoints return a `cursor`, which is `null` on the last page. Pass a non-null one back as `?cursor=` to fetch the next page.

```bash
curl -H 'Authorization: Bearer ship-your-api-key' \
  'https://api.shipstatic.com/deployments?limit=50'
```

```json
{
  "deployments": [...],
  "cursor": "eyJpZCI6ImhhcHB5LWNhdC1hYmMxMjM0In0"
}
```

### Custom domain — full flow

```bash
# 1. Validate the name
curl -X POST -H 'Authorization: Bearer ship-your-api-key' \
  https://api.shipstatic.com/domains/www.example.com/validate

# 2. Create + link to a deployment
curl -X PUT -H 'Authorization: Bearer ship-your-api-key' \
  -H 'Content-Type: application/json' \
  -d '{"deployment":"happy-cat-abc1234"}' \
  https://api.shipstatic.com/domains/www.example.com

# 3. Get required DNS records
curl -H 'Authorization: Bearer ship-your-api-key' \
  https://api.shipstatic.com/domains/www.example.com/records

# 4. After configuring DNS, trigger verification
curl -X POST -H 'Authorization: Bearer ship-your-api-key' \
  https://api.shipstatic.com/domains/www.example.com/verify
```

### Claim a public deployment

```bash
curl -X POST https://api.shipstatic.com/account/claim \
  -H 'Authorization: Bearer ship-your-api-key' \
  -H 'Content-Type: application/json' \
  -d '{"claim":"<claim-hash-from-deploy-response>"}'
```

Returns the claimed deployment, now bound to your account.

## Multipart upload shape (`POST /deployments`)

| Field | Type | Notes |
|-------|------|-------|
| `files[]` | File parts | One per file. The filename carries the path (`webkitRelativePath` style) |
| `checksums` | JSON-encoded `string[]` | One MD5 per file, in the same order as `files[]` |
| `labels` | JSON-encoded `string[]` | Optional |
| `password` | string | Optional, 6–128 characters |
| `via` | string | Optional origin tag (`cli`, `sdk`, `mcp`, etc.) |

## Status Codes

| Code | Meaning | Example |
|------|---------|---------|
| `200` | Success | Read, update, synchronous delete |
| `201` | Created | `POST /deployments`, `POST /tokens` |
| `202` | Accepted | `DELETE /deployments/:deployment` (async cleanup) |
| `400` | Bad request | Invalid input, missing fields, or a request the platform refuses |
| `401` | Authentication error | Missing or invalid credentials |
| `403` | Forbidden | Account terminated, plan limit reached, operation not permitted |
| `404` | Not found | Resource doesn't exist or belongs to another account |
| `409` | Conflict | A domain already taken, a domain operation already in progress, a deployment a domain still points at |
| `413` | Payload too large | Bundle exceeds plan limits |
| `415` | Unsupported media type | A deploy sent as neither `multipart/form-data` nor `application/json` |
| `422` | Unprocessable | The project failed to build, or the resource's state doesn't allow this operation |
| `429` | Rate limited | Too many requests; retry after `Retry-After` seconds |
| `500` | Server error | Something failed on our side |
| `503` | Unavailable | The platform is in maintenance, or a service it depends on is briefly unavailable |

## Errors

Every error answers the same shape:

```json
{
  "error": "not_found",
  "message": "Deployment not found",
  "status": 404
}
```

`error` is the type tag, and one of these:

| Tag | Status | Meaning |
|-----|--------|---------|
| `validation_failed` | `400` | The input is malformed |
| `authentication_failed` | `401` | Credentials are missing or invalid |
| `forbidden` | `403` | Authenticated, but not allowed to do this |
| `not_found` | `404` | The resource doesn't exist, or isn't yours |
| `rate_limit_exceeded` | `429` | Too many requests; wait `Retry-After` seconds |
| `business_logic_error` | `400`, `409`, `413`, `415`, `422` | Well formed, but the platform refuses it; the message says why |
| `build_failed` | `422` | The project could not be built. `details.log` carries the tail of the build output. Fix the project; retrying gives the same answer |
| `maintenance` | `503` | The platform is closed on purpose. The message says when it reopens, so wait rather than retrying in a loop |
| `internal_server_error` | `500`, `503` | Something failed on our side; a later retry may succeed |

`message` is written for the person using your tool, so show it as it is. Some errors add a `details` object with structured context.

**Branch on `error` and `status`, never on the message text.** Messages are rewritten for clarity; the tag and the status are the contract.

## Pagination

List endpoints (`/deployments`, `/domains`, `/tokens`, `/activities`) return cursor-paginated results:

```json
{ "deployments": [...], "cursor": "..." }
```

Pass the cursor as `?cursor=...` to fetch the next page. `cursor` is always present, and `null` on the final page. Default page size is 50; pass `?limit=N` to override (capped at 300).

## Rate Limits

Per-account global limiter; deploy endpoints additionally limited per-credential to prevent abuse. On exhaustion, the API returns `429` with a `Retry-After` header (seconds). Anonymous public deploys have a tighter per-IP limit than authenticated requests.

## Idempotent Deploys

Deploy endpoints accept an optional `Idempotency-Key` header (up to 256 characters — a UUID is a good choice). If a request with a key succeeds and is retried within 24 hours, the API replays the original `201` response instead of creating a duplicate deployment; replayed responses carry `Idempotency-Replay: true`. Use it whenever your client retries on timeout. Failed attempts are not stored — retrying after an error runs the deploy fresh.

## Conventions

**Content type.** All requests and responses use `application/json`. Deployment upload also accepts `multipart/form-data`.

**Domain names** in paths are URL-encoded. The API normalizes casing and Unicode (e.g. `Example.COM` → `example.com`).

**Async operations.** Only deployment deletion is asynchronous (returns `202`). All other deletes are synchronous (`200`).

**Errors** share one shape and one set of tags; see [Errors](https://docs.shipstatic.com/api#errors).
