# SDK

> Publish and manage sites from Node.js or the browser: the client, deploy options, every resource method, events, errors and pagination.

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

Publish a site from your own code, in Node.js or the browser — three lines, and no credentials needed to start. [NPM](https://www.npmjs.com/package/@shipstatic/ship) / [GIT](https://github.com/shipstatic/ship)

```bash
npm install @shipstatic/ship
```

## Quick Start

```js
import Ship from '@shipstatic/ship';

const ship = new Ship({ token: 'ship-your-api-key' });
const deployment = await ship.deploy('./dist');
```

## Deploy to your account

**Deploying needs no account.** `new Ship({})` publishes, and the response carries a claim URL. Everything else — listing, custom domains, account operations — needs a credential: pass an [API Key](https://docs.shipstatic.com/api-key) or a [deploy token](https://docs.shipstatic.com/tokens) as the constructor's `token` option. The option is called `token` and your API key is what goes in it — one credential, two names.

**There is one credential slot.** `token` carries any platform token, and the value's prefix says what it is — so there is no precedence to reason about.

```js
// No account — public deploys, response includes a claim URL
new Ship({});

// API key — persistent, full account access
new Ship({ token: 'ship-your-api-key' });

// Deploy token — scoped to deploys, optional TTL, revocable
new Ship({ token: 'deploy-your-token' });

// Any opaque bearer, e.g. an OAuth access token — sent verbatim
new Ship({ token: accessToken });

// A provider — invoked per request, so refresh stays with you
new Ship({ token: () => mintToken() });

// Cookie session — first-party browser apps only
new Ship({ session: true });

// Set or rotate after construction
ship.setToken('ship-your-api-key');
```

`token` and `session` together is a configuration error — one client, one identity.

## Deploy Options

```ts
await ship.deploy('./dist', {
  labels: ['production'],
  password: 'hunter2!',
  ttl: 604800,
  idempotencyKey: process.env.GITHUB_RUN_ID,
  signal: AbortSignal.timeout(30_000),
  spaDetect: false,
  pathDetect: false,
});
```

`password` (6–128 characters) protects the deployment behind an unlock page. It travels over TLS and is stored securely — it can never be read back. To change or remove a password, redeploy — there is no edit-in-place.

`ttl` is a lifetime in **seconds**, after which the platform reclaims the deployment. It needs a credential (a deploy made without one already expires on the platform's own schedule) and cannot be combined with a domain.

`idempotencyKey` makes a deploy replayable rather than repeatable: if a call times out you cannot tell "it never landed" from "it landed and the response was lost", and retrying without a key creates a second deployment. Key the *attempt* — a run id, a commit sha, a uuid minted before the first try.

`signal` is the one cancellation mechanism.

## Resources

### Deployments

```ts
ship.deployments.upload(path, options?)
ship.deployments.list(options?)
ship.deployments.get(id)
ship.deployments.set(id, { labels })
ship.deployments.delete(id)
```

### Domains

```ts
ship.domains.set(name, { deployment?, labels? })
ship.domains.list(options?)
ship.domains.get(name)
ship.domains.delete(name)
ship.domains.validate(name)
ship.domains.verify(name)
ship.domains.dns(name)
ship.domains.records(name)
ship.domains.share(name)
```

`domains.set()` is a single upsert operation. Omitted fields are preserved on update:

```ts
ship.domains.set('www.example.com');                                         // Reserve
ship.domains.set('www.example.com', { deployment: 'happy-cat-abc1234' });    // Link
ship.domains.set('www.example.com', { deployment: 'shy-fox-def5678' });      // Switch
ship.domains.set('www.example.com', { labels: ['prod'] });                   // Label only
```

Once linked, a domain cannot be unlinked — switch to a different deployment or delete the domain.

### Tokens

```ts
ship.tokens.create({ ttl?, labels? })
ship.tokens.list(options?)
ship.tokens.get(token)
ship.tokens.delete(token)
```

### Account

```ts
ship.account.get()     // or ship.whoami()
ship.ping()            // { timestamp }: the server's clock, in unix seconds
ship.getLimits()       // { maxFileSize, maxFilesCount, maxTotalSize, blockedExtensions? }
```

## Browser

The browser SDK accepts `File[]` only. Convert `FileList` from an `<input>` element with `Array.from()`. Construct `File` objects directly when you have raw content.

```js
import Ship from '@shipstatic/ship';

const ship = new Ship({ token: 'ship-your-api-key' });

// From file input or drag-and-drop
await ship.deploy(Array.from(fileInput.files));

// From raw content — construct File objects
await ship.deploy([
  new File(['<html>...</html>'], 'index.html', { type: 'text/html' }),
  new File(['body { ... }'], 'styles.css', { type: 'text/css' }),
]);
```

## Events

Subscribe to lifecycle events for logging and metrics.

```js
ship.on('request', (url, init) => console.debug('→', init.method, url));
ship.on('retry', (error, url, attempt) => console.debug('↻', attempt, url));
ship.on('response', (response, url) => console.debug('←', response.status, url));
ship.on('error', (error, url) => console.error('✗', url, error));
```

`request` fires once per attempt. A failed attempt that will be tried again emits `retry`; `error` is terminal and fires exactly once per failed call. So one call emits `retry* (error | response)` and the stream is unambiguous at every prefix — you never have to wait to learn what you are watching.

## Error Handling

`@shipstatic/ship` re-exports the error types — you don't need to install `@shipstatic/types` separately.

```js
import Ship, { isShipError, ErrorType } from '@shipstatic/ship';

try {
  await ship.deploy('./dist');
} catch (error) {
  if (isShipError(error)) {
    error.isAuthError();        // semantic category
    error.isNetworkError();     // semantic category
    error.isClientError();      // any 4xx, or a local File, Config or Cancelled failure
    error.type === ErrorType.File;        // specific-type check
    error.type === ErrorType.Validation;  // specific-type check
  }
}
```

## TypeScript

Full type safety with exported types:

```ts
import type {
  ShipClientOptions, DeploymentOptions, ShipEvents,
  Deployment, Domain, Account, StaticFile,
} from '@shipstatic/ship';
```

## Pagination

Every `list()` takes `{ limit?, cursor? }` and returns one page. The response
carries the next `cursor`, or `null` when the collection is exhausted.

```js
let cursor = null;
do {
  const page = await ship.deployments.list({ limit: 50, cursor });
  for (const deployment of page.deployments) { /* … */ }
  cursor = page.cursor;
} while (cursor);
```

## Configuration

The SDK resolves credentials in this order:

1. Constructor options
2. Environment variables: `SHIP_TOKEN`, `SHIP_API_URL`

The SDK reads no files at all — `~/.shiprc` is the CLI's business. This means `new Ship({})` is safe to use from embedded contexts (MCP, library wrappers) without inheriting the host developer's dotfile. A host that needs strict isolation scrubs `SHIP_TOKEN` from the process; there is deliberately no opt-out flag, because a flag you can forget to set is how credentials leak.

`SHIP_PASSWORD` is read by the CLI only — pass `password` directly via the `deploy()` options when calling the SDK programmatically.
