# Posting

Publishing is the one thing here that a person cannot undo. It needs the `posts:write` scope, which is granted separately from everything else on purpose, and it is worth being deliberate about.

The loop that works:

1. `GET /api/v1/connections` to see what the brand can post to at all.
2. `POST /api/v1/posts/validate` with the exact body you intend to send.
3. Read every verdict. Fix what is wrong, or drop the account that is broken.
4. `POST /api/v1/posts`.
5. Read every result. A partial run is normal, not an error to retry wholesale.

## Platform rules

These come from the same values the code enforces, so a caption that fits here fits at the platform.

| Platform | Accepts | Title cap | Caption cap | Posting modes | Posting cap |
| --- | --- | --- | --- | --- | --- |
| TikTok | video and photos | 90 characters | 4000 characters | `draft`, `direct` | 15 posts per 24 hours |
| YouTube | video only | 100 characters | 5000 characters | `direct` | no cap enforced here |
| Instagram | video only | no title | 2200 characters | `direct` | no cap enforced here |
| LinkedIn | video only | no title | 3000 characters | `direct` | no cap enforced here |

- **TikTok.** Slideshows go out as a photo post. Draft mode drops the post into the TikTok inbox so a person finishes it in the app.
- **YouTube.** Uploads are published publicly unless the brand changed its YouTube visibility setting in ReelMoney.
- **Instagram.** Posts publish as reels from a public video URL, so render the video before you post it.
- **LinkedIn.** LinkedIn rejects a video smaller than 75 KB or larger than 500 MB.

A slideshow and a video are not interchangeable. TikTok takes a slideshow as a photo post directly, with no render needed. Instagram, YouTube and LinkedIn take video, so render the slideshow first with `POST /api/v1/slideshows/{id}/render` and post it when the render job succeeds.

## Connecting an account

An account is connected once, by a person, in a browser. The API cannot do it alone and does not pretend to.

```bash
curl -X POST https://reel.money/api/v1/connections/link \
  -H "Authorization: Bearer rm_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "platform": "tiktok" }'
```

The response carries `connect_url`, the `instructions` to relay and a note that the link goes stale. Then:

1. Give the link to the person who owns the account. Do not open it yourself.
2. They sign in on the platform and approve the permissions ReelMoney asks for.
3. They land back on ReelMoney with the account attached to this brand.
4. Call `GET /api/v1/connections` and confirm the account appears with a healthy token.

What each platform expects of the person:

- **TikTok.** The account they want to post from. ReelMoney asks for permission to upload and publish videos and photo posts, and to read the account's basic profile.
- **YouTube.** A Google account with the channel they want to upload to. ReelMoney asks for upload and read access, nothing else.
- **Instagram.** A business or creator account. Personal accounts cannot publish through the platform's API at all, which is a rule on their side.
- **LinkedIn.** A member account, and the organization pages they are an admin of if they want to post as a company.

Build the link when the person is ready to click it, because authorize links expire in minutes. A platform this deployment has no client id for answers 503 `platform_not_configured`, in which case connect from the dashboard instead.

## Validate before you post

`POST /api/v1/posts/validate` takes the same body as the post, needs only `posts:read`, enqueues nothing and touches nothing. It checks the things that actually go wrong:

- Does the short exist, and does it have a video or slides to publish yet.
- Is each account connected, and is its token still fresh.
- Are the caption and the title inside that platform's caps.
- Is there posting headroom left for that account right now.
- Is `scheduled_at` at least 5 minutes ahead.

```json
{
  "ok": false,
  "short_id": 8842,
  "scheduled_at": "2026-08-19T15:00:00Z",
  "issues": [],
  "verdicts": [
    {
      "social_data_id": 55,
      "platform": "tiktok",
      "account_name": "northwindcoffee",
      "token_status": "expired",
      "rate_limit": { "used": 3, "max": 15, "remaining": 12, "limited": false, "label": "15 posts per 24 hours" },
      "ok": false,
      "issues": [
        { "code": "token_expired", "message": "The tiktok connection reports expired and needs a fresh connect link.", "retryable": false }
      ]
    }
  ]
}
```

`ok` at the top is true only when every verdict is true. A validate that costs one request saves a post that half worked.

## Publishing

```bash
curl -X POST https://reel.money/api/v1/posts \
  -H "Authorization: Bearer rm_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "short_id": 8842,
    "platforms": ["tiktok"],
    "scheduled_at": "2026-08-19T15:00:00Z"
  }'
```

| Field | Notes |
| --- | --- |
| `short_id` | Required. The short must have a rendered video or slides. |
| `social_data_ids` | Specific connected accounts, from `GET /api/v1/connections`. |
| `platforms` | Every connected account of these platforms. Use this or `social_data_ids`. |
| `scheduled_at` | ISO 8601. Leave it out to publish now. At least 5 minutes ahead when present. |
| `post_format` | For example `video` or `slideshow`. ReelMoney picks a sensible default per short type. |
| `posting_mode` | TikTok only. `direct` publishes, `draft` drops the post into the TikTok inbox for a person to finish in the app. |
| `auto_add_music` | TikTok photo posts. Lets TikTok add music. |
| `platform_customizations` | Per account title and caption overrides, keyed by `social_data_id` as a string. |

Send an `Idempotency-Key` header. A create that timed out is then safe to retry without posting twice.

### Scheduling

A scheduled post needs at least 5 minutes of lead time, which is what the queue needs to pick it up reliably. Anything sooner, or in the past, answers `schedule_too_soon`. Send the time in ISO 8601 with an explicit zone, and remember the platform caps still apply at the moment the post goes out, not at the moment you scheduled it.

Scheduled posts show up in `GET /api/v1/posts` with status `scheduled` and their `scheduled_at`.

### Cancelling

`DELETE /api/v1/posts/{id}` pulls back a post that has not gone out. Posts in `draft`, `pending` or `scheduled` can be cancelled. Anything already publishing or published answers 409 `not_cancellable`, because the platform has it now and we cannot take it back.

## Partial success

Posting to several accounts is not all or nothing. One expired token never stops the other accounts.

```json
{
  "short_id": 8842,
  "overall_status": "partial",
  "results": [
    { "social_data_id": 55, "platform": "tiktok", "account_name": "northwindcoffee", "share_id": 9901, "status": "pending", "scheduled_at": null },
    { "social_data_id": null, "platform": "instagram", "share_id": null, "status": "failed",
      "error": { "code": "platform_not_connected", "message": "No instagram account is connected.", "retryable": false } }
  ]
}
```

- `overall_status` is `success` when every account worked, `partial` when some did, `failed` when none did.
- Walk `results` and handle each entry on its own. Retry only the accounts that failed, and only when their error is retryable.
- Never resend the whole request because one account failed. That is how the same video ends up on TikTok twice.
- A `share_id` is the handle for that account's post. It is what `DELETE /api/v1/posts/{id}` takes and what shows up in `GET /api/v1/posts`.

## Watching what happened

`GET /api/v1/posts` lists posts newest first, filtered by `short_id`, `status` or `platform`. Statuses are `draft`, `pending`, `scheduled`, `retrying`, `success`, `failed` and `cancelled`. A successful post carries the link on the platform once it lands.

A day or two later, `GET /api/v1/analytics` reports how it did. TikTok is the platform with an analytics adapter today, and the payload says so in `supported_platforms`.
