# Culacc API

Store files, publish them to the web, serve resized images from EU-hosted
storage, and let your visitors edit photos and videos in a Culacc window. One
REST API, an npm package, a browser script, and webhooks when things change.

Base address: `https://culacc.com/api/v1`

## Getting a key

Settings → Developers → **New key**. A key looks like `clc_live_…`, is shown
once, and carries permissions you choose:

| Permission | Lets a key |
|---|---|
| `files:read` | list and read files |
| `files:write` | upload and replace files, start editor sessions |
| `links:write` | publish files to the web and change links |
| `webhooks:write` | manage webhooks |

Send it as a header:

```bash
curl -H "Authorization: Bearer clc_live_..." https://culacc.com/api/v1/me
```

`X-Api-Key: clc_live_…` works too. Each key has a per-minute rate limit and can
be locked to particular addresses. Keys belong on a server: anything in a
browser is public.

## The npm package

```bash
npm install @culacc/sdk
```

```js
const { Culacc } = require('@culacc/sdk');
const culacc = new Culacc({ key: process.env.CULACC_KEY });

const { files } = await culacc.files.list({ kind: 'image', limit: 10 });
const { link, html } = await culacc.files.publish(files[0].id);

console.log(link.url);   // https://media.culacc.com/aB3.../photo.jpg
console.log(html);       // <picture> with srcset, ready to paste
```

It retries 429s and 5xx with a growing gap, times out at 60 seconds, and throws
`CulaccError` with `status` and `code` on anything else. Version 1.1 adds
`culacc.edit` for the editor.

## Files

### List files

```
GET /v1/files?kind=image&limit=50
```

| Parameter | Meaning |
|---|---|
| `path` | browse a folder, e.g. `/Photos` |
| `search` | search names instead of browsing |
| `kind` | `image`, `svg`, `video`, `audio`, `pdf`, comma separated |
| `limit`, `offset` | paging; up to 200 at a time |

With neither `path` nor `search` you get the most recent files.

```json
{
  "files": [{
    "id": 4821, "name": "hero.jpg", "path": "/Photos/hero.jpg",
    "kind": "image", "mime": "image/jpeg", "size": 2280114,
    "updatedAt": "2026-09-14T08:12:00.000Z",
    "published": true, "url": "https://media.culacc.com/aB3.../hero.jpg"
  }],
  "folders": [{ "name": "Photos", "path": "/Photos" }],
  "nextOffset": null
}
```

### One file

```
GET /v1/files/4821
```

Returns the file, its link if it has one, and ready-made HTML.

## Publishing

Publishing gives a file a stable public address. Nothing is public until you
ask for it, and turning a link off stops it immediately.

```
POST /v1/files/4821/publish
{ "allowedHosts": ["example.com"] }     // optional
```

```json
{
  "link": { "slug": "aB3...", "url": "https://media.culacc.com/aB3.../hero.jpg",
            "kind": "image", "status": "active", "width": 3000, "height": 2000 },
  "html": "<picture><source type=\"image/webp\" srcset=\"...\"><img src=\"...\" width=\"3000\" height=\"2000\" loading=\"lazy\"></picture>"
}
```

- `PATCH /v1/files/4821/publish` with `{"status":"off"}` switches it off, or
  changes `allowedHosts`.
- `DELETE /v1/files/4821/publish` removes the link for good.
- `GET /v1/links` lists everything published.

**Allowed sites** are checked against the browser's `Referer` and `Origin`.
They discourage other sites from embedding your files; they aren't a lock,
since anyone with the address can still open it directly. Leave them off for
files you place in Miro or other tools that fetch from their own servers.

## Images on the way out

Add parameters to any published image address:

| Parameter | Meaning |
|---|---|
| `w` | width in pixels; sizes are snapped to a fixed set |
| `h` | height in pixels |
| `format` | `webp`, `jpeg`, `png`, or `auto` for whatever the browser prefers |
| `q` | quality, 1 to 100 |
| `fit` | `cover`, `contain` or `inside` |

```
https://media.culacc.com/aB3.../hero.jpg?w=800&format=webp
```

The SDK builds these for you:

```js
culacc.image(link.url, { width: 800, format: 'webp' })
```

Versioned addresses (`?v=…`, which the generated HTML uses) are cached for a
year. Videos stream with range requests, so seeking works.

## Uploading

Bytes never pass through the API server: you ask for an address, the browser or
your server PUTs to it, then you say it's done.

```js
const file = await culacc.upload(buffer, {
  name: 'photo.jpg', size: buffer.length, mime: 'image/jpeg', path: '/Photos',
});
```

By hand, if you'd rather:

```
POST /v1/uploads        { "name": "photo.jpg", "size": 2280114, "mime": "image/jpeg" }
→ { "uploadId": "…", "uploadUrl": "https://…", "method": "PUT", "expiresIn": 900 }

PUT <uploadUrl>         (the bytes)
POST /v1/uploads/<uploadId>/done
→ { "file": { "id": 4822, "name": "photo.jpg", "size": 2280114 } }
```

The size recorded is what storage actually received, not what was claimed.
Calling `done` twice returns the same file rather than making two. Uploads
expire after 15 minutes and count against storage, not traffic.

**From a browser:** have your own server call `POST /v1/uploads` and hand the
page only the `uploadUrl`. Your key stays on your server. If you'd rather not
build an upload form at all, an editor session without a `fileId` gives your
visitor one (see Edit with Culacc).

## Edit with Culacc

Put an **Edit** or **Open** button on your own site. Your visitor clicks it, a
Culacc window opens over your page, they pick an app, edit their photo or
video, and the result is saved in your Culacc storage and sent back to your
page. Documents work too: Word, Excel and PowerPoint files open in Google Docs,
Sheets and Slides. You decide which apps they see; they never need a Culacc
account.

| App | Files | What it does |
|---|---|---|
| `image-editor` | images | crop (free or 1:1, 4:3, 3:4, 16:9, 9:16), rotate, flip, brightness, contrast, saturation; runs in the visitor's browser |
| `video-editor` | videos | trim start and end, crop to square, 16:9, 9:16 or 4:5, remove the sound; processed on Culacc's servers |
| `google` | documents, spreadsheets, presentations | opens the file in Google Docs, Sheets or Slides in a new tab, in the visitor's own Google account, and saves it back in its own format |
| `viewer` | images, videos, audio, PDFs | opens the file as it is |

When only one app fits the file, the editor opens it straight away. Canva,
Adobe Express and Microsoft 365 are coming; like Google, they'll ask the visitor
to sign in to their own account.

Without a `fileId`, the visitor adds a file: from their device, or from their
own **Google Drive**, **Dropbox** or **OneDrive**.

### How it works

- Your server asks Culacc for an editor session with your API key, and gets an `editorUrl` back.
- Your page opens that address with the Culacc script: as a window over the page, a popup, or a full-page redirect.
- The visitor edits and saves. The result replaces the file's contents in your storage (or becomes a new version, if you keep history) and is published.
- Your page gets the result at once in `onSave`. Your server also gets the `file.edited` webhook, even if the visitor closed the tab.

Your API key never reaches the browser. The address the page gets works only
for that one session, for 30 minutes, and only inside the sites you allowed.

### Set it up in Culacc

In Culacc, open **Settings → Developers** and scroll to **Edit with Culacc**, below your API keys:

- **Allowed sites**: the sites that may open the editor, like `https://example.com`. Plain `http://` works for `localhost` while you build. Nothing opens until you add one.
- **Apps**: which ones your visitors see, in what order, and which one is suggested for photos, videos and documents.
- **Where visitors can add a file from**: their device always; Google Drive, Dropbox and OneDrive as you choose.
- **When a visitor saves**: replace the file, keep the original, or keep every version (see Versions below).
- **What comes back**: image format (same as the original, JPEG, WebP, AVIF or PNG), largest width, quality, video format (MP4 or WebM) and largest height.
- **Limits**: how many saves are processed at once, largest photo, video and document, longest clip.
- **Look**: your accent colour and title, and whether to show "Editing by Culacc".

The same section shows this month's sessions, saves, editing time and cost, the code to copy for your site (and warns if none of your keys can start sessions), and your recent editor sessions.

### Start a session (your server)

The key needs `files:write`.

```js
const { editorUrl } = await culacc.edit.createSession({
  origin: 'https://example.com',
  fileId: 4821,                 // leave out to let the visitor add a new photo or video
  externalRef: 'user-42',       // your own id; comes back in the result and the webhook
});
```

Or by hand:

```
POST /v1/edit/sessions
{ "origin": "https://example.com", "fileId": 4821, "externalRef": "user-42" }
```

```json
{
  "session": { "id": "es_Xk2…", "status": "open", "mode": "edit", "fileId": 4821,
               "apps": ["image-editor", "viewer"], "versioning": "off",
               "expiresAt": "2026-10-17T19:45:00.000Z" },
  "token": "est_…",
  "editorUrl": "https://culacc.com/api/edit/open/es_Xk2…#t=est_…"
}
```

| Field | Meaning |
|---|---|
| `origin` | required: the site that opens the editor; must be in your allowed sites |
| `fileId` | the file to edit or open; without it, the visitor adds a new one |
| `mode` | `edit` (default) or `open` |
| `accept` | what a visitor may add without a `fileId`: `image`, `video`, `document`; all three by default |
| `apps` | some of the apps you switched on, in the order to show them: `image-editor`, `video-editor`, `google`, `viewer` |
| `versioning` | `off`, `keep-original` or `keep-all`, instead of your default |
| `publish` | publish the result and return its address; on by default |
| `folder` | where added files go; `/Embeds` by default, in a subfolder named after `externalRef` |
| `externalRef` | your own id, up to 200 characters |
| `returnUrl` | where to send the visitor after a redirect; must be on `origin` |
| `ttlMinutes` | how long the session can be opened, 5 to 120; 30 by default |

Make one session per click: a session can be saved once.

### Open it (your page)

```html
<script src="https://culacc.com/api/edit/sdk.js"></script>
<button id="edit-photo">Edit photo</button>
<script>
  CulaccEditor.attach('#edit-photo', {
    session: () => fetch('/api/culacc-session', { method: 'POST' }).then((r) => r.json()),
    onSave: (result) => { avatar.src = result.url + '?v=' + result.revision; },
    onCancel: () => {},
    onError: (err) => console.warn(err.message),
  });
</script>
```

`attach` asks your server for a session on each click, then opens the editor.
If you already have an `editorUrl`, open it yourself:

```js
const { done } = CulaccEditor.open(editorUrl, { onSave, display: 'modal' });
const { status, result } = await done;   // 'saved' or 'cancelled'
```

| Option | Meaning |
|---|---|
| `display` | `modal` (default; full screen on phones), `popup` (a window of its own) or `redirect` (the tab goes to Culacc and comes back to `returnUrl`) |
| `onSave(result)` | the visitor saved; see the result below |
| `onCancel()` | the visitor closed the editor without saving |
| `onError(err)` | something failed; the editor has already told the visitor |
| `onReady(info)` | the editor loaded: `{ mode, apps }` |
| `onAdded(file)` | the visitor added a new file: `{ fileId, name, kind }` |
| `title` | an accessible label for the window |

With `redirect`, the visitor comes back to your `returnUrl` with
`culacc_session`, `culacc_status` and `culacc_file` in the query string. Read
the outcome from your server with `culacc.edit.getSession(id)` rather than
trusting the address.

### The result

The same object arrives in `onSave`, in the `file.edited` webhook and in
`GET /v1/edit/sessions/:id` as `result`:

```json
{
  "fileId": 4821, "name": "avatar.webp", "path": "/Photos/avatar.webp",
  "kind": "image", "mime": "image/webp", "size": 48211,
  "width": 1080, "height": 1080, "duration": null,
  "edited": true, "added": false, "version": 3, "previousVersion": 0,
  "versioning": "keep-original", "app": "image-editor",
  "url": "https://media.culacc.com/aB3…/avatar.webp",
  "revision": "4821-3-lq9x2k"
}
```

- `url` stays the same across edits, so add `?v=` + `revision` to skip old copies in caches.
- `edited` is false when the visitor chose "use it as it is".
- `added` is true when the visitor added a new file in this session.
- When the output format differs from the original, the file is renamed to match (`avatar.jpg` → `avatar.webp`) but keeps its `fileId`.
- The webhook adds `sessionId`, `externalRef` and `origin`.

Trust the webhook, or `getSession` from your server, for anything that
matters: `onSave` runs in the visitor's browser.

### Sessions from your server

```
GET  /v1/edit/sessions/:id          files:read
POST /v1/edit/sessions/:id/cancel   files:write
```

`status` is `open`, `processing` (a video is being cut), `saved`, `cancelled`,
`failed` (with `error`) or `expired`.

### Documents in Google Docs, Sheets & Slides

Pass the `fileId` of a `.docx`, `.xlsx`, `.pptx`, `.odt`, `.ods`, `.odp`, `.rtf`,
`.txt`, `.csv`, `.doc`, `.xls` or `.ppt` file, or let the visitor add one. The editor
shows **Open in Google Docs** (or Sheets, or Slides). Then:

- A new tab asks the visitor to sign in to **their own** Google account and allow Culacc to create one file in their Drive (the `drive.file` permission: Culacc can't see anything else there).
- Culacc puts a copy there, converted to Google's format, and the tab opens Google's editor.
- The visitor edits, comes back to the Culacc window and presses **Save**.
- Culacc exports the file back to its own format (a `.docx` stays a `.docx`; old `.doc`, `.xls` and `.ppt` come back as `.docx`, `.xlsx` and `.pptx`, renamed, same `fileId`), saves it like any other edit, deletes the copy from their Drive and revokes the sign-in.

Cancelling, discarding or leaving the session to run out deletes the copy and
revokes the sign-in too; nothing is kept on either side. Your site's own Google
account is never involved. The session stays open up to 8 hours while the file
is in Google, so long edits can still be saved.

Formatting that Google doesn't support in the original format can change on
the way back, the same as when you open such a file in Google yourself.

### Adding a file from cloud storage

When there's no `fileId`, the visitor can add a file from their device or pick
one from their own Google Drive, Dropbox or OneDrive, using that service's own
file picker and their own sign-in. Culacc copies just that one file into your
storage (Google's own formats arrive as `.docx`, `.xlsx` or `.pptx`), and it then
opens like any file. You choose which of these to offer in Settings.

### Versions

History is **off** by default: an edit replaces the file and the old picture
is deleted, so it costs no storage. Turn it on in Settings, or per session:

| `versioning` | What's kept |
|---|---|
| `off` | only the latest edit |
| `keep-original` | the original upload as version 0, plus the latest edit |
| `keep-all` | every edit |

```js
const { versions } = await culacc.edit.versions(4821);
await culacc.edit.revert(4821, 0);        // back to the original
```

```
GET  /v1/edit/files/:id/versions    files:read
POST /v1/edit/files/:id/revert      { "version": 0 }    files:write
```

A revert deletes nothing, so it can be undone, and sends `file.edited` with
`reverted: true`.

### What it costs

You pay for what your visitors use, end to end; visitors pay nothing.

- **Storage**: the files, and their versions if you keep them, at your usual storage price.
- **Traffic**: everything the editor moves counts as media traffic, 10 GB a month free, then €1 per TB. Bytes that go straight between the visitor and storage count once: a file the visitor adds, a video streamed into the trimmer, the edited picture uploaded. Bytes that pass through Culacc's servers count twice, coming in and going out: the photo loaded into the image editor, a file added from Google Drive, Dropbox or OneDrive, a document sent to Google and the one coming back, and the file a save reads in plus the result it writes back.
- **Editing time**: saving takes processing time, measured per save. The first 60 CPU minutes a month are free, then €0.20 per CPU hour. Saving a photo takes about a second; a minute of 1080p video takes roughly one to three CPU minutes.

There is one meter and one monthly spending limit for everything: published files, the API, every integration and the editor. Settings → Developers → **This month** shows the total, split into traffic and editing. When the limit is
reached, published files stop loading and new editor sessions are refused with a
message to the visitor, until next month or until you raise it; nothing is
deleted. Without a card on file,
the editor stops when the free allowances run out.

Saves wait in line rather than all running at once: each account processes two
at a time by default, and the editor shows the visitor how many are ahead.

```
GET /v1/edit/usage                  files:read
```

### Good to know

- The editor runs on `culacc.com` inside a frame only your allowed sites can show, so your page's styles and scripts can't reach into it, and it can't reach into your page.
- Photos in HEIC can't be edited in most browsers; the visitor can still use them as they are.
- Google opens in a new tab rather than inside the editor, because Google doesn't allow its editors inside other sites.
- Limits you can change in Settings: photos up to 40 MB and videos up to 500 MB by default; clips up to 10 minutes.

## Webhooks

```js
const { webhook } = await culacc.webhooks.create({
  url: 'https://example.com/culacc',
  events: ['file.published', 'file.uploaded', 'file.edited'],
});
console.log(webhook.secret);    // whsec_… — shown once
```

| Event | When |
|---|---|
| `file.published` | a file got a public link |
| `file.unpublished` | a link was removed |
| `file.uploaded` | an API upload finished |
| `file.edited` | a visitor saved in the editor, or a file was reverted (`reverted: true`) |
| `traffic.limit_reached` | the monthly spending limit stopped serving |

Each delivery is a POST with a signature:

```
Culacc-Event: file.published
Culacc-Signature: t=1789862786,v1=9f2a…
```

The signature is an HMAC-SHA256 of `timestamp.body` using your secret, so you
can tell a real call from a fake one and spot a replay. Verify it with the raw
body, before any parsing:

```js
const { verifyWebhook } = require('@culacc/sdk');

app.post('/culacc', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyWebhook(process.env.CULACC_WEBHOOK_SECRET, req.get('Culacc-Signature'), req.body)) {
    return res.sendStatus(400);
  }
  const { event, data } = JSON.parse(req.body);
  if (event === 'file.edited') {
    // data.externalRef is what you passed to createSession; data.url is the new picture
  }
  res.sendStatus(200);      // answer quickly, work afterwards
});
```

A failed delivery is retried at once, then after 5 seconds, then after 30. A
4xx answer stops the retries, since that won't fix itself. A webhook that keeps
failing is switched off, and switching it back on clears the count.

## Traffic and what it costs

```
GET /v1/usage
```

```json
{ "month": "2026-09-01", "bytes": 48216115, "free": 10737418240,
  "cost": { "amount": 0, "currency": "eur" },
  "limit": { "amount": 10, "currency": "eur" }, "stopped": false }
```

10 GB of traffic a month is free, then €1 per TB, counted to the cent and added
to your existing Culacc invoice. Every user has a spending limit they control
in Settings; when it's reached, published files stop serving until the next
month, and nothing is deleted. `traffic.limit_reached` fires when that happens.

Uploads and storage are billed separately, as storage. Editing time in the
editor is its own line on the invoice but counts toward the same spending limit,
so files stop serving when traffic and editing together reach it; see
`GET /v1/edit/usage`.

## Errors

Ordinary HTTP codes, with a body:

```json
{ "error": "This key is missing the links:write permission.", "code": "FORBIDDEN" }
```

| Code | When |
|---|---|
| 400 | something in the request is wrong |
| 401 | missing, revoked or unknown key |
| 403 | the key lacks a permission, the address isn't allowed, or the site isn't in your editor's allowed sites |
| 404 | no such file, link, session or endpoint |
| 409 | the editor session was already saved or cancelled |
| 410 | the editor session expired |
| 413 | the file or clip is too large or too long |
| 415 | no switched-on app can open that kind of file |
| 429 | over the key's rate limit (`Retry-After` says how long), or the monthly spending limit is reached (`LIMIT_REACHED`) |

## Limits

| Thing | Limit |
|---|---|
| Keys per account | 25 |
| Requests per key | as set per key, 120 a minute by default |
| Browser-direct upload | 500 MB per file |
| Webhooks per account | 10 |
| Files per listing | 200 |
| Editor session | 30 minutes to open by default, up to 120; saved once |
| Editor files | photos 40 MB, videos 500 MB, documents 50 MB, clips 10 minutes by default; adjustable in Settings |
| Adding from cloud storage | up to 200 MB per file, and never more than the limit for that kind |
| Editor saves processed at once | 2 per account by default; the rest wait in line |

## Questions

Email <andrej@culacc.com> — you'll get a reply from the person who built it.
