# Endpoints

Base URL `https://reel.money`. Every endpoint needs `Authorization: Bearer rm_...`. The full contract, with every field and every example, is [/openapi.json](/openapi.json). This page is the map.

Every brand facing endpoint also takes an optional `brand_id` query parameter (everything except `GET /api/v1/account`, `GET /api/v1/capabilities` and `GET /api/v1/errors/{code}`). Omit it and the request acts on the key's own brand. A key with the `brands:all` scope may name any brand the account owns, ids from `GET /api/v1/brands`; see [authentication](/api-docs/authentication) for the rules.

| Group | Endpoints | Scope |
| --- | --- | --- |
| Discovery | account, brand, brands, capabilities, errors | `brand:read` |
| Media | direct_uploads, media, media_packs, background_music | `media:read`, `media:write` |
| Generation | slideshows, videos, render | `content:write` |
| Jobs and shorts | jobs, shorts | `brand:read` |
| Connections | connections, connections/link | `brand:read` |
| Posting | posts, posts/validate | `posts:read`, `posts:write` |
| Analytics | analytics, analytics accounts, refresh | `analytics:read` |

## Discovery

Read these before you build a request. They are cheap and they stop most mistakes.

| Endpoint | What it answers |
| --- | --- |
| `GET /api/v1/account` | Who the key belongs to, the plan, the slideshow quota used and remaining, credits when the plan shows them, the brands the owner can reach, and the scopes this key holds. |
| `GET /api/v1/brand` | The brand profile (name, description, website, language, categories), the slideshow settings that shape every generation, and which platforms are connected. |
| `GET /api/v1/brands` | The brands the key owner belongs to, with counts. Generation always happens in the key's own brand. |
| `GET /api/v1/capabilities` | The enums and limits the API accepts. Any valid key may call it. |
| `GET /api/v1/errors/{code}` | The cause, the fix and whether a retry helps, for any code the API publishes. |

`GET /api/v1/capabilities` is the one to read before generating. It carries the design styles, the image worlds, the overlay fonts, the aspect ratios, the slide count bounds, the short types, the image sources with whether AI images are affordable right now, the platform rules, and the poll guidance. The values come from the same code that enforces them, so it cannot lie.

### Design styles

Leave `design_style` out of a create and ReelMoney picks a recipe the brand has not used lately, which is what keeps a feed varied. Pin one only when a caller asks for a specific look.

| Key | Name | Look |
| --- | --- | --- |
| `classic_stroke` | Classic TikTok stroke | White lowercase text with a black outline, the native TikTok caption look. |
| `clean_pills` | Clean pill stack | Short lines inside crisp white pills over soft photos, polished and easy to save. |
| `bold_statement` | Bold statement caps | Huge condensed capitals that read like a motivational poster. |
| `editorial_serif` | Editorial serif | Elegant serif headlines with a small caption low on the frame, quiet magazine feel. |
| `handwritten_diary` | Handwritten diary | Everything in a handwriting font, like a page torn out of a personal journal. |
| `typewriter_notes` | Typewriter notes | Typewriter text anchored high on the slide, like the first lines of a private note. |
| `accent_pop` | Accent color pop | One loud accent color on the key lines, plain white text everywhere else. |
| `black_card` | Black card | Near black pills with white text, sleek and declarative. |
| `whisper_minimal` | Whisper minimal | One tiny caption low on the frame so the photo carries the slide. |
| `serif_quote_cards` | Serif quote cards | Every slide is a centered serif quote, made to be reposted to a story. |
| `numbered_bold` | Numbered bold list | Giant list numbers as the design, with one tight point under each. |
| `duo_serif_sans` | Serif + sans duo | An elegant serif headline paired with a much smaller clean sans support line. |

### Image worlds

The photographic world a generation lands in. Chosen automatically alongside the design style.

| Key | Name | Imagery |
| --- | --- | --- |
| `pinterest_girl` | Pinterest lifestyle (faceless woman) | Lifestyle photos of a woman, always faceless or from behind, in soft natural light. |
| `cozy_interior` | Cozy interiors | Warm interiors with candles, linen beds, steaming mugs and rain on the window. |
| `moody_luxury` | Moody luxury | Dark luxury still life with black marble, watches, leather and city lights at night. |
| `nature_escape` | Nature escape | Big calm landscapes: misty mountains, ocean cliffs, forest light and alpine lakes. |
| `city_nights` | City nights | Night streets with neon reflections, wet asphalt, light trails and rain bokeh. |
| `minimal_flatlay` | Minimal objects & flatlays | Clean objects and flatlays on plain backgrounds with lots of empty space for text. |
| `gym_discipline` | Gym & discipline | Gritty training photos: dark gyms, chalk dust, early runs, faceless athletes. |
| `coastal_summer` | Coastal summer | Sun drenched coastal scenes with turquoise water, white sand and bright streets. |

### Aspect ratios

`9:16` at 1080 by 1920 (default), `1:1` at 1080 by 1080, `16:9` at 1080 by 608, `4:5` at 1080 by 1350, `2:3` at 1080 by 1620

## Media

Getting a file in takes three moves, in this order:

1. **Presign.** `POST /api/v1/direct_uploads` with `filename`, `byte_size`, `content_type` and, when you have it, a base64 MD5 `checksum`. Answers 201 with a `signed_id` and a `direct_upload` object holding a `url` and the exact `headers` to use. Scope `media:write`.
2. **Upload.** `PUT` the raw bytes to that `url` with those headers, exactly as given. This request goes to storage, not to ReelMoney, and carries no API key.
3. **Register.** `POST /api/v1/media` with `{"file": "<signed_id>"}` plus an optional `title`, `media_type` (`user_upload` or `product_demo`) and `tags`. Answers 201 with the media record. Scope `media:write`.

Skip step three and the upload is eventually swept away, because a blob with no owning record is not media yet.

Limits: images up to 10 MB, videos up to 200 MB. Accepted types are jpeg, png, webp, gif, heic, heif, mp4, webm and quicktime. A file over the cap answers `media_too_large`, a type outside the list answers `unsupported_media_type`.

```bash
curl -X POST https://reel.money/api/v1/direct_uploads \
  -H "Authorization: Bearer rm_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "kitchen-b-roll.mp4", "byte_size": 8412332, "content_type": "video/mp4" }'
```

Reading media, all under scope `media:read`:

| Endpoint | What it lists |
| --- | --- |
| `GET /api/v1/media?source=own` | The media this brand owns. `query` matches title and tags. |
| `GET /api/v1/media?source=packs` | The media inside the packs the owner can reach. |
| `GET /api/v1/media?source=stock` | The shared global library. Here `query` is a meaning based image search, so describe the picture rather than guessing keywords. |
| `GET /api/v1/media?source=stock&media_type=avatar_video` | The avatar clips a hook and demo video can use. |
| `GET /api/v1/media_packs` | The packs this account can use, with a preview. Pass an id as `image_source.media_pack_id`. |
| `GET /api/v1/background_music` | The tracks a video can name through `background_music_id`. |

## Generation

Every create answers 202 with a job envelope, never with finished content, and every create takes an `Idempotency-Key` header. All of them need `content:write`.

### POST /api/v1/slideshows

Writes the slides, art directs them and sources an image for each one.

| Field | Notes |
| --- | --- |
| `context` | Required, up to 3500 characters. Say who it is for and what it should get across. The single biggest lever on quality. |
| `slide_count` | `0` lets the model choose the length, otherwise send 3 to 12. |
| `aspect_ratio` | One of the ratios above. Defaults to `9:16`. |
| `image_source` | `{"type": "stock"}`, `{"type": "pack", "media_pack_id": 12}` or `{"type": "ai"}`. |
| `design_style` | A key from the design styles table. Leave it out for variety. |
| `title`, `caption`, `tags`, `language` | Optional. `tags` is a comma separated string. |

`image_source.type` of `ai` generates a fresh image per slide and spends credits. Without enough credits it answers 402 `insufficient_credits`, and `stock` or `pack` cost nothing. Over the plan's monthly slideshow cap it answers 403 `quota_exceeded`, which `GET /api/v1/account` would have told you first.

### PATCH /api/v1/slideshows/{id}

Edit a generated slideshow: `title`, `caption`, `tags`, `aspect_ratio`, gradient settings, or the slides themselves. Submitted text overlays are normalised server side the same way generated ones are, so fonts, sizes, positions and contrast stay inside what the renderer supports. `PUT` behaves the same for clients that cannot send PATCH.

Only a settled slideshow may be edited. While a generation or a render is running it answers 409 `job_in_progress`.

### POST /api/v1/slideshows/{id}/render

`{"format": "video"}` turns a slideshow into an MP4. Use this when a caller wants a video of a slideshow they already have, instead of generating a second one. Answers a job of kind `render`; the file is ready when the job succeeds and `result.video_url` is filled in.

A slideshow that already has a video, or one with a render in flight, answers 409. Posting a slideshow to TikTok as a photo post does not need a render.

### POST /api/v1/videos

A hook and demo video: an avatar clip speaks the hook, then the product demo clips play.

| Field | Notes |
| --- | --- |
| `hook_text` | The line the avatar speaks and shows on screen. |
| `avatar_media_id` or `avatar_url` | Required. Browse `GET /api/v1/media?source=stock&media_type=avatar_video`, or upload your own clip. |
| `product_demo_media_ids`, `product_demo_urls` | The demo clips, played in the order given. |
| `background_music_id` | An id from `GET /api/v1/background_music`. |
| `text_settings`, `loop_count`, `title`, `caption` | Optional. |

An avatar is required and is checked before anything is queued, so a missing one answers 422 `validation_failed` instead of rendering a black video half an hour later.

## Jobs and shorts

`GET /api/v1/jobs/{id}` is the one poller for every kind of long running work, under scope `brand:read`. The job id is the short id the create returned.

```json
{
  "job_id": 8842,
  "kind": "slideshow_generation",
  "status": "processing",
  "status_message": "Generating the slideshow content.",
  "poll_after_seconds": 15,
  "estimated_completion_at": "2026-08-18T09:16:02Z"
}
```

The contract, in four lines:

- `status` is one of `queued`, `processing`, `succeeded`, `failed`. Nothing else.
- Wait `poll_after_seconds` between polls. It is repeated in the `Retry-After` header. Typical intervals: `slideshow_generation` every 15 seconds, `video_generation` every 30 seconds, `render` every 30 seconds.
- Stop the moment the status reads `succeeded` or `failed`. A finished job does not change again.
- A failed job carries `error` with a code from the catalog and whether a retry can help. A succeeded job carries `result` with `short_id` and any `preview_url`, `video_url` or slide count.

Shorts, also `brand:read`:

- `GET /api/v1/shorts` lists this brand's slideshows and videos, newest first, filtered by `short_type` (`SLIDESHOW`, `UGC`, `all`), `status`, `created_after` and `created_before`.
- `GET /api/v1/shorts/{id}` returns one in full: the type data with the slides and overlays, the URLs, the thumbnail and every post made from it.

## Connections

Connecting a social account needs a person at a browser. The API hands over the link and the steps rather than pretending otherwise. Both endpoints need `brand:read`.

- `GET /api/v1/connections` lists the accounts this brand can post to. Each one reports its token status, its platform posting headroom and a single `ready_to_post` flag. Platforms with no account are listed under `not_connected`. Access tokens never leave the server, so no token appears in the payload.
- `POST /api/v1/connections/link` with `{"platform": "tiktok"}` (or `youtube`, `instagram`, `linkedin`) answers with `connect_url`, the `instructions` to relay and an `expires_note`.

The steps, which the response repeats in plain words:

1. Give `connect_url` to the person who owns the account. Do not try to follow it yourself.
2. They sign in to the platform and approve the permissions. They land back on ReelMoney with the account attached to this brand.
3. Call `GET /api/v1/connections` again and confirm the account shows up as active.

Authorize links go stale after a few minutes, so build one when the person is ready to click it. A platform this deployment has no client id for answers 503 `platform_not_configured`.

## Posting

Covered in full on the [posting page](/api-docs/posting), including the platform rules and the partial success model.

| Endpoint | Scope | What it does |
| --- | --- | --- |
| `POST /api/v1/posts/validate` | `posts:read` | Dry run. Same body as the post. Enqueues nothing, answers one verdict per account. |
| `POST /api/v1/posts` | `posts:write` | Publish now, or schedule with `scheduled_at`. Answers 202 with a partial success report. |
| `GET /api/v1/posts` | `posts:read` | Posts and scheduled posts, newest first. Filter by `short_id`, `status`, `platform`. |
| `DELETE /api/v1/posts/{id}` | `posts:write` | Cancel a post that has not gone out. Anything already published answers 409 `not_cancellable`. |

## Analytics

All three need `analytics:read`. TikTok is the platform with an analytics adapter today, and `supported_platforms` in the payload says so. Accounts on other platforms are simply not listed.

- `GET /api/v1/analytics` gives a summary per connected account.
- `GET /api/v1/analytics/accounts/{social_data_id}` gives post by post metrics plus the account summary. `period` is `24h`, `7d`, `30d` or `90d`, `filter=reelmoney` narrows to posts published through ReelMoney, and `sort` accepts `posted_at` or any of the count columns with `dir`.
- `POST /api/v1/analytics/accounts/{social_data_id}/refresh` pulls fresh numbers and answers 202. Inside its cooldown it answers 429 with the seconds left in `details.retry_after_seconds`. That is a platform friendliness limit, separate from the key ceiling.
