# FotoMagico Document REST API — Reference

External reference for the FotoMagico data-model REST API: enable it, authenticate, and query or
construct `.fms` slideshow documents programmatically. This is the **canonical** endpoint + DTO
reference — for a hands-on walkthrough see [TESTING.md](TESTING.md); for internal architecture see
[README.md](README.md).

> **Maintainers:** this document is the public contract. When the API gains or changes endpoints or
> DTOs, update this file (and [TESTING.md](TESTING.md)) in the same change.

API version: **v1** · Status: Phase 1 (structural backbone).

---

## 1. Base URL & versioning

```
http://127.0.0.1:50000/v1
```

The server is local-only (loopback) and runs inside the FotoMagico macOS app. The API is
**path-versioned**: all endpoints live under `/v1`. Breaking changes will ship under `/v2` while
`/v1` keeps working, so pin to a version in your base URL.

## 2. Enabling the API

The API is **off by default**. Enable it and read the access token from the app's user defaults
(bundle id `com.boinx.FotoMagico6.macOS`):

```bash
# enable
defaults write com.boinx.FotoMagico6.macOS FMWebServer-editingAPIEnabled -bool YES
# read the auto-generated bearer token
defaults read com.boinx.FotoMagico6.macOS FMWebServer-apiToken
```

These settings are intended to be surfaced in FotoMagico's Preferences. The app must be running.

## 3. Authentication

Every `/v1` request requires a bearer token:

```
Authorization: Bearer <token>
```

Missing/invalid token → `401 unauthorized`. (The token is generated once and persists; treat it as a
secret.)

## 4. Conventions

- **Content type:** request and response bodies are JSON. Send `Content-Type: application/json` on
  requests with a body.
- **Identifiers:** documents are addressed by `id` (the document's settings UUID). Slides, layers,
  chapters, and audio objects are addressed by their stable `uuid`. The literal id **`current`**
  resolves to the most recently active/created document (works even while the app is in the
  background).
- **Patch semantics:** `PATCH` bodies are partial — only the fields you include are changed; omitted
  fields are left untouched. `PUT` (chapter only) is create-or-replace.
- **Status codes:** `200` OK · `201` Created · `204` No Content · `400` bad request (malformed body /
  params) · `401` unauthorized · `403` media folder not authorized · `404` not found · `405` method
  not allowed · `409` conflict · `500` internal error.
- **Errors:** every failed request returns a JSON envelope:

  ```json
  { "error": "not_found", "message": "No open document with id 'abc'.", "details": null }
  ```

  Branch on the machine-readable `error` code, not the human `message`. Codes: `bad_request`,
  `unauthorized`, `authorization_required`, `not_found`, `method_not_allowed`, `conflict`,
  `internal_error`.

## 5. Media & the sandbox

FotoMagico is sandboxed, so it can only read media files it has been granted access to. Media is
referenced by **percent-encoded `file://` URL** (e.g. `file:///Users/me/Pictures/sky.jpg`).

- Grant access to a folder once via `POST /v1/media/authorize-folder` (opens a folder chooser in the
  app); access is **recursive** over that folder's subtree and persists across launches.
- If you reference a file outside every authorized folder, the request fails with
  `403 authorization_required` and a `details.path` of the offending URL — authorize its parent
  folder, then retry.
- Text layers need no media and no authorization.

---

## 6. Endpoints

All paths are relative to the base `http://127.0.0.1:50000/v1`.

### Documents

| Method | Path | Request body | Response |
|--------|------|--------------|----------|
| GET    | `/documents` | — | `200` [`FMDocumentSummaryDTO`] |
| POST   | `/documents` | `FMCreateDocumentDTO` | `201` `FMDocumentSummaryDTO` (opens an editor window) |
| GET    | `/documents/{docId}` | — | `200` `FMDocumentDTO` (deep: settings + slides) |
| DELETE | `/documents/{docId}` | — | `204` · `409` if unsaved (pass `?force=true` to discard) |
| POST   | `/documents/{docId}/save` | `FMSavePathDTO` | `200` `FMSavePathDTO` |

Notes: `POST /documents/{docId}/save` with `{}` saves in place (requires the document to have a prior
on-disk location, else `409`); pass `{"path":"…"}` to save to a specific location (must be writable
under the sandbox).

### Media authorization

| Method | Path | Request body | Response |
|--------|------|--------------|----------|
| GET    | `/media/authorized-folders` | — | `200` [`FMAuthorizedFolderDTO`] |
| POST   | `/media/authorize-folder` | — | `200` `FMAuthorizedFolderDTO` · `409` if cancelled |

`POST /media/authorize-folder` presents a modal folder chooser in the app and blocks until the user
responds — call it interactively, not from an unattended client.

### Settings

| Method | Path | Request body | Response |
|--------|------|--------------|----------|
| GET    | `/documents/{docId}/settings` | — | `200` `FMSettingsDTO` |
| PATCH  | `/documents/{docId}/settings` | `FMSettingsPatchDTO` | `200` `FMSettingsDTO` |

### Slides

| Method | Path | Request body | Response |
|--------|------|--------------|----------|
| GET    | `/documents/{docId}/slides` | — | `200` [`FMSlideDTO`] |
| POST   | `/documents/{docId}/slides?index={n}` | `FMCreateSlideDTO` | `201` `FMSlideDTO` |
| GET    | `/documents/{docId}/slides/{slideId}` | — | `200` `FMSlideDTO` |
| PATCH  | `/documents/{docId}/slides/{slideId}` | `FMSlidePatchDTO` | `200` `FMSlideDTO` |
| DELETE | `/documents/{docId}/slides/{slideId}` | — | `204` |
| POST   | `/documents/{docId}/slides/reorder` | `FMReorderDTO` | `200` [`FMSlideDTO`] |

`POST /slides` appends by default; `?index={n}` inserts at position `n` (0-based, clamped). The
optional `layers` array in `FMCreateSlideDTO` seeds layers on the new slide. `reorder` requires the
`order` array to be a permutation of the current slide uuids (else `409`/`400`).

### Transition (one per slide)

| Method | Path | Request body | Response |
|--------|------|--------------|----------|
| GET    | `/documents/{docId}/slides/{slideId}/transition` | — | `200` `FMTransitionDTO` |
| PATCH  | `/documents/{docId}/slides/{slideId}/transition` | `FMTransitionPatchDTO` | `200` `FMTransitionDTO` |

### Chapter (zero or one per slide)

| Method | Path | Request body | Response |
|--------|------|--------------|----------|
| GET    | `/documents/{docId}/slides/{slideId}/chapter` | — | `200` `FMChapterDTO` · `404` if none |
| PUT    | `/documents/{docId}/slides/{slideId}/chapter` | `FMChapterPatchDTO` | `200` `FMChapterDTO` (create-or-update) |
| PATCH  | `/documents/{docId}/slides/{slideId}/chapter` | `FMChapterPatchDTO` | `200` `FMChapterDTO` · `404` if none |
| DELETE | `/documents/{docId}/slides/{slideId}/chapter` | — | `204` |

`PUT` creates the chapter if the slide has none, then applies the fields. `PATCH` only updates an
existing chapter (404 otherwise).

### Layers (visual: image / video / text)

| Method | Path | Request body | Response |
|--------|------|--------------|----------|
| GET    | `/documents/{docId}/slides/{slideId}/layers` | — | `200` [`FMLayerDTO`] |
| POST   | `/documents/{docId}/slides/{slideId}/layers?index={n}` | `FMCreateLayerDTO` | `201` `FMLayerDTO` |
| GET    | `/documents/{docId}/slides/{slideId}/layers/{layerId}` | — | `200` `FMLayerDTO` |
| PATCH  | `/documents/{docId}/slides/{slideId}/layers/{layerId}` | `FMLayerPatchDTO` | `200` `FMLayerDTO` |
| DELETE | `/documents/{docId}/slides/{slideId}/layers/{layerId}` | — | `204` |

`image`/`video` layers require a `file://` `mediaURL` (see §5); `text` layers require a `text` string
(styled with the app's default font/color/alignment). `map` layers cannot be created via this API
version. Layer index is the stacking order on the slide.

### Audio (per slide)

| Method | Path | Request body | Response |
|--------|------|--------------|----------|
| GET    | `/documents/{docId}/slides/{slideId}/audio` | — | `200` [`FMAudioDTO`] |
| POST   | `/documents/{docId}/slides/{slideId}/audio` | `FMCreateAudioDTO` | `201` `FMAudioDTO` |
| PATCH  | `/documents/{docId}/slides/{slideId}/audio/{audioId}` | `FMAudioPatchDTO` | `200` `FMAudioDTO` |
| DELETE | `/documents/{docId}/slides/{slideId}/audio/{audioId}` | — | `204` |

### Effects (per slide and per layer)

Effects are **fixed slots** — you don't create or delete them, you enable + configure them
(`{"enabled":false}` turns one off). The same shape applies to a slide and to a layer. `{effectType}`
is an `FMEffectKind` value (unknown → `404`).

| Method | Path | Request body | Response |
|--------|------|--------------|----------|
| GET    | `/documents/{docId}/slides/{slideId}/effects` | — | `200` [`FMEffectDTO`] |
| GET    | `/documents/{docId}/slides/{slideId}/effects/{effectType}` | — | `200` `FMEffectDTO` |
| PATCH  | `/documents/{docId}/slides/{slideId}/effects/{effectType}` | `FMEffectPatchDTO` | `200` `FMEffectDTO` |
| GET    | `/documents/{docId}/slides/{slideId}/layers/{layerId}/effects` | — | `200` [`FMEffectDTO`] |
| GET    | `/documents/{docId}/slides/{slideId}/layers/{layerId}/effects/{effectType}` | — | `200` `FMEffectDTO` |
| PATCH  | `/documents/{docId}/slides/{slideId}/layers/{layerId}/effects/{effectType}` | `FMEffectPatchDTO` | `200` `FMEffectDTO` |

**Effect catalog.** Every effect has `enabled` (Bool) and `amount` (Double, 0…1). Additional
parameters by `type` (a `PATCH` includes only the ones you want to change):

| `type` | parameters |
|--------|------------|
| `colorCorrection` | `tint`(`#RRGGBBAA`), `saturation`, `contrast`, `gamma` |
| `grayscale` | `red`, `green`, `blue` |
| `sepia` | `tone` |
| `blur` | `radius` |
| `vignette` | `factor`, `radius`, `feather` |
| `stroke` | `width`, `placement`(`inside`/`centered`/`outside`), `color`(`#RRGGBBAA`) |
| `shadow` | `blurRadius`, `offsetX`, `offsetY`, `color`(`#RRGGBBAA`) |
| `comic`, `watercolor`, `etching`, `woodcut` | `smoothing`, `detail`, `inkStroke`, `inkFill`, `colorSteps`, `colorFill`, `algorithm` |
| `mask` | `maskType`(`none`/`rect`/`oval`/`image`), `maskWidth`, `maskHeight`, `feather`, `opacity` |
| `background` | `shape`(`box`/`speechBubble`/`thoughtBubble`/`actionBubble`), `color`(`#RRGGBBAA`), `paddingX`, `paddingY`, `cornerRadius`, `arrowVectorX`, `arrowVectorY`, `arrowWidth`, `arrowBend`, `spikeLength`, `spikeJitter` |
| `film` | `grain`, `scratches`, `blotches`, `jitter`, `flicker` |
| `video` | `lineWidth`, `lineContrast`, `flicker` |
| `lensDistortion` | `centerX`, `centerY`, `factor`, `radius` |
| `chromaKey` | `despillColor`(`#RRGGBBAA`), `despillFactor` (hue/sat/luma masking is envelope-based — later phase) |
| `blendMode` | `mode`(`normal`/`add`/`subtract`/`lighten`/`darken`) |
| `backgroundRemoval` | `quality`(`accurate`/`balanced`/`fast`) |
| `cubeLUT` | `fileURL` (`file://` URL of a `.cube` LUT; imported under the sandbox folder grants like other media) |

---

## 7. Data types (DTOs)

JSON objects. In create/patch types, **optional** fields may be omitted (patch = leave unchanged;
create = use the model default). Colors are `#RRGGBBAA` hex strings. Times are seconds (Double).

### Document

**FMDocumentSummaryDTO** — `id:String`, `title:String`, `subtitle:String`, `author:String`,
`organization:String`, `slideCount:Int`, `isCurrent:Bool`.

**FMDocumentDTO** — `id:String`, `settings:FMSettingsDTO`, `slides:[FMSlideDTO]`.

**FMCreateDocumentDTO** — `title:String?`, `width:Int?`, `height:Int?`, `backgroundColor:String?`.

**FMSavePathDTO** — `path:String?` (request: target file path, or omit to save in place; response:
the path saved to).

### Settings

**FMSettingsDTO** — `id:String`, `width:Int`, `height:Int`, `backgroundColor:String`, `loops:Bool`,
`title:String`, `subtitle:String`, `notes:String`, `author:String`, `organization:String`,
`copyright:String`, `url:String`, `email:String`.

**FMSettingsPatchDTO** — all of the above except `id`, each optional.

### Slide

**FMSlideDTO** — `uuid:String`, `index:Int`, `name:String`, `notes:String`, `duration:Double`,
`continueType:Int` (see §8), `animated:Bool`, `transition:FMTransitionDTO`, `chapter:FMChapterDTO?`,
`layers:[FMLayerDTO]`, `audio:[FMAudioDTO]`.

**FMCreateSlideDTO** — `duration:Double?`, `continueType:Int?`, `animated:Bool?`,
`transition:FMTransitionPatchDTO?`, `layers:[FMCreateLayerDTO]?`.

**FMSlidePatchDTO** — `name:String?`, `notes:String?`, `duration:Double?`, `continueType:Int?`,
`animated:Bool?`.

**FMReorderDTO** — `order:[String]` (all current slide uuids, in the desired order).

### Transition

**FMTransitionDTO** — `type:Int` (see §8), `duration:Double`.
**FMTransitionPatchDTO** — `type:Int?`, `duration:Double?`.

### Chapter

**FMChapterDTO** — `uuid:String`, `name:String`, `color:String` (`#RRGGBBAA`, or `""` if none),
`notes:String`, `loops:Bool`, `expanded:Bool`.
**FMChapterPatchDTO** — `name:String?`, `color:String?` (empty string clears the color), `notes:String?`,
`loops:Bool?`, `expanded:Bool?`.

### Layer

**FMTransformDTO** — `x:Double`, `y:Double` (stage coordinates), `zoom:Double` (scale relative to the
stage), `rotation:Double` (degrees).

**FMLayerDTO** — `uuid:String`, `kind:FMLayerKind` (see §8), `opacity:Double` (0…1),
`startTransform:FMTransformDTO`, `finishTransform:FMTransformDTO`, `mediaURL:String?` (image/video
only), `text:String?` (text only).

**FMCreateLayerDTO** — `kind:FMLayerKind`, `mediaURL:String?` (required for image/video),
`text:String?` (required for text), `opacity:Double?`, `startTransform:FMTransformDTO?`,
`finishTransform:FMTransformDTO?`. With no transforms, image/video get a default fit + Ken Burns
animation.

**FMLayerPatchDTO** — `opacity:Double?`, `startTransform:FMTransformDTO?`,
`finishTransform:FMTransformDTO?`, `text:String?` (text layers only; preserves existing styling).

### Audio

**FMAudioDTO** — `uuid:String`, `name:String`, `track:Int` (see §8), `mediaURL:String?`,
`volume:Double` (0…1), `inPoint:Double`, `outPoint:Double` (0 = natural end), `fadeIn:Double`,
`fadeOut:Double`.

**FMCreateAudioDTO** — `track:Int`, `mediaURL:String` (required, `file://`), `volume:Double?`,
`inPoint:Double?`, `outPoint:Double?`.

**FMAudioPatchDTO** — `volume:Double?`, `inPoint:Double?`, `outPoint:Double?`, `fadeIn:Double?`,
`fadeOut:Double?`.

### Effect

Each effect kind has its **own small DTO** (only its own fields) rather than one big union. Reads
return `type` + `enabled` + `amount` + that kind's parameters; the read DTOs are
`FMColorCorrectionEffectDTO`, `FMGrayscaleEffectDTO`, `FMSepiaEffectDTO`, `FMBlurEffectDTO`,
`FMVignetteEffectDTO`, `FMStrokeEffectDTO`, `FMShadowEffectDTO`, `FMStylizeEffectDTO`
(comic/watercolor/etching/woodcut), `FMMaskEffectDTO`.

For a `PATCH`, the body carries the **common** fields `enabled:Bool?` + `amount:Double?` plus any of
that kind's parameters (the matching `…EffectPatchDTO`); `type` comes from the URL path, not the body.
Parameters that don't apply to the effect are ignored. The exact parameters per `type` are the effect
catalog in §6.

### Media & errors

**FMAuthorizedFolderDTO** — `url:String` (`file://` URL of an authorized folder).
**FMAPIErrorDTO** — `error:String` (machine code), `message:String`, `details:[String:String]?`.

---

## 8. Enumerations

**FMLayerKind** (string): `"image"`, `"video"`, `"text"`, `"map"` (map not creatable in v1).

**FMEffectKind** (string): `colorCorrection`, `grayscale`, `sepia`, `blur`, `vignette`, `stroke`,
`shadow`, `comic`, `watercolor`, `etching`, `woodcut`, `mask`, `background`, `film`, `video`,
`lensDistortion`, `chromaKey`, `blendMode`, `backgroundRemoval`, `cubeLUT`. Effect-parameter enums:
`placement` (stroke) `inside`/`centered`/`outside` · `maskType` (mask) `none`/`rect`/`oval`/`image` ·
`shape` (background) `box`/`speechBubble`/`thoughtBubble`/`actionBubble` · `mode` (blendMode)
`normal`/`add`/`subtract`/`lighten`/`darken` · `quality` (backgroundRemoval) `accurate`/`balanced`/`fast`.

**Slide `continueType`** (Int) — how a slide advances:
`0` slide duration · `1` user event (click/keypress) · `2` audio marker · `3` network event.

**Transition `type`** (Int, `FMTransitionType.rawValue`):
`0` cut · `1` fade · `2` dissolve · `3` linear wipe · `4` slide · `5` push · `6` flip · `7` cube ·
`8` circular wipe · `9` angular wipe · `10` twirl · `11` zoom · `12` aperture · `13` shuffle ·
`14` topple · `15` thumb-through · `16` flash · `17` pixelate · `18` blinds · `19` broken tube ·
`20` barn door · `21` blur · `22` page curl. (`-1`/`-2` are reserved magic values.)

**Audio `track`** (Int): `0` music · `1` effects · `2` narration.

---

## 9. Example: build a slideshow

```bash
API="http://127.0.0.1:50000/v1"
AUTH="Authorization: Bearer $(defaults read com.boinx.FotoMagico6.macOS FMWebServer-apiToken)"
JSON="Content-Type: application/json"

# Create a document (capture its id)
DOC=$(curl -s -X POST "$API/documents" -H "$AUTH" -H "$JSON" \
  -d '{"title":"My Slideshow","width":1920,"height":1080}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')

# Add a slide (capture its uuid)
curl -s -o /dev/null -X POST "$API/documents/$DOC/slides" -H "$AUTH" -H "$JSON" -d '{"duration":6}'
SLIDE=$(curl -s -H "$AUTH" "$API/documents/$DOC/slides" | python3 -c 'import sys,json;print(json.load(sys.stdin)[0]["uuid"])')

# Add a title text layer, set a dissolve transition
curl -s -o /dev/null -X POST "$API/documents/$DOC/slides/$SLIDE/layers" -H "$AUTH" -H "$JSON" \
  -d '{"kind":"text","text":"Welcome"}'
curl -s -o /dev/null -X PATCH "$API/documents/$DOC/slides/$SLIDE/transition" -H "$AUTH" -H "$JSON" \
  -d '{"type":2,"duration":1.5}'

# Inspect the result
curl -s -H "$AUTH" "$API/documents/$DOC" | python3 -m json.tool
```

To add image/video layers, first authorize the containing folder
(`POST /v1/media/authorize-folder`), then reference files by `file://` URL.

---

## 10. Versioning policy

The current version is **v1**. Additive changes (new optional DTO fields, new endpoints) may land in
`v1` without notice. Breaking changes (removing/renaming fields, changing semantics) ship under a new
prefix (`/v2`); `/v1` remains available for existing clients. Always include the version in your base
URL.
