> ## Documentation Index
> Fetch the complete documentation index at: https://docs.slideless.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload from a script or CI

> Publish a presentation from a script, CI pipeline, or any non-interactive context with the slideless CLI

## When to use this

You want to upload a presentation from somewhere other than the dashboard or a Claude session. For example:

* A CI/CD job that publishes a release deck on every tag
* A cron task that re-publishes a daily metrics deck
* Any custom integration

The interactive path is documented in [Use Slideless with Claude](/guides/with-claude). This guide is the script-friendly path.

## Prerequisites

* Node.js 20+ on the runner
* An **organization API key** with `presentations:write` (see [API keys](/concepts/api-keys))
* The deck you want to share — a folder or a single `.html` file, within your plan's size caps (see [Presentations](/concepts/presentations#size-and-file-count-caps))

## Install the CLI on the runner

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install -g slideless
```

If global install is awkward in your CI environment, use `npx slideless ...` instead.

## Authenticate non-interactively

Pass the key through an environment variable. The CLI reads `SLIDELESS_API_KEY` automatically when no profile is configured:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export SLIDELESS_API_KEY="cko_your_key_here"
```

Or save it to a profile in non-interactive mode:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless login --api-key "$SLIDELESS_API_KEY" --name ci
```

## Push a folder

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless push ./my-deck --title "Q4 review" --json
```

Output:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "success": true,
  "data": {
    "presentationId": "0192f1c3-...",
    "version": 1,
    "role": "owner",
    "isNew": true,
    "assetsUploaded": 5,
    "assetsDeduped": 0,
    "totalBytes": 194000
  }
}
```

Extract the presentationId:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
SHARE_ID=$(slideless push ./my-deck --title "Q4 review" --json | jq -r '.data.presentationId')
echo "Pushed: $SHARE_ID"
```

A fresh push creates the deck but does not publish it — viewer URLs only exist once you mint a token:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
SHARE_URL=$(slideless share "$SHARE_ID" --json | jq -r '.data.shareUrl')
echo "Published: $SHARE_URL"
```

The exit code is non-zero on failure, so the CI step fails fast if the upload errors.

### Single-file alternative

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless push ./deck.html --title "Q4 review" --json
```

Both forms work interchangeably — same endpoints, same response shape.

### Strict mode for CI

In CI you probably want unresolved references (typos, missing files) to fail the build rather than just warn:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless push ./my-deck --title "Q4 review" --strict --json
```

## Re-publish to the same URL

Ensure the deck folder still contains its `slideless.json` (written by the first push), then:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless push ./my-deck --json
```

The CLI reads `slideless.json`, sends `expectedBaseVersion`, and commits a new version to the same `presentationId`. Only changed files re-upload — the rest are deduplicated.

If the CI runner doesn't persist the working copy between runs (fresh clone each time), you have two choices:

1. Commit `slideless.json` into your repo so it comes back with each clone.
2. Save the `presentationId` in a CI secret and `slideless pull "$SHARE_ID" ./my-deck` before running your build step, then `slideless push ./my-deck --json`.

If another push landed between your last pull and this one, you'll get `409 conflict`. Pull and retry, or pass `--force` to overwrite.

## The upload protocol under the hood

`slideless push` is a thin wrapper around three HTTP endpoints:

1. **[`POST /precheckAssets`](/api-reference/precheck-assets)** — send a list of SHA-256 hashes, get back which ones the backend doesn't have.
2. **[`POST /uploadPresentationAsset`](/api-reference/upload-asset)** — multipart upload of one missing blob (content-addressed; server re-verifies the hash).
3. **[`POST /commitPresentationVersion`](/api-reference/commit-presentation-version)** — send the manifest (list of `{path, sha256, size, contentType}`), backend validates every hash exists, writes the manifest, bumps `currentVersion`.

The CLI runs them in order and prints progress between each step. See the linked API reference pages for the request/response shapes if you ever need to implement the flow in a language that isn't Node.

## Errors

The CLI maps backend HTTP statuses to short codes. Common ones:

| `code`              | Meaning                                                                                                                            |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `unauthenticated`   | Missing or invalid key                                                                                                             |
| `permission-denied` | Key valid but missing `presentations:write`                                                                                        |
| `payload-too-large` | File or total deck exceeds the plan cap                                                                                            |
| `too-many-files`    | Manifest exceeds the plan's max file count                                                                                         |
| `blobs-missing`     | Manifest references a hash not in the store (shouldn't happen in normal use — indicates the CLI or a custom client skipped a step) |
| `invalid-argument`  | Bad path, bad sha256 format, etc.                                                                                                  |
| `internal`          | Backend 5xx; retry with backoff                                                                                                    |

Static-scan errors emit `invalid-argument` with a details list of the offending references (e.g. `../outside/foo.jpg`).

## Two URLs, two purposes

Don't confuse the **share URL** (what you give recipients) with the **direct deck URL** (what the dashboard iframe embeds):

| Purpose                          | URL                                                                                                       | When                                                                       |
| -------------------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| **Share URL** for recipients     | `https://app.slideless.ai/share/{presentationId}?token={token}`                                           | What `slideless share <id>` returns. Send this to people.                  |
| **Direct deck URL** for iframing | `https://europe-west1-slideless-ai.cloudfunctions.net/getSharedPresentation/{presentationId}/_t/{token}/` | Loads the entry HTML; relative asset paths (`./foo.jpg`) resolve natively. |

The share URL wraps the direct deck URL in chrome (download button, fullscreen toggle, footer). The direct URL is the raw rendering surface.

## Next

* **[CLI command reference](/cli/commands)** — Every command and flag.
* **[Collaboration](/concepts/collaboration)** — Cross-device push/pull and the conflict model.
* **[Working with assets](/guides/working-with-assets)** — Folder layout, ignore rules, relative path behavior.
