---
name: "Rawugc API"
repo: https://github.com/tfcbot/rawugc-skills
source: https://github.com/tfcbot/rawugc-skills/blob/HEAD/skills/rawugc-api/SKILL.md
license: "Apache-2.0"
install: "npx skills add tfcbot/rawugc-skills --skill rawugc-api"
page: https://100xlab.com/resources/agent-skills/tfcbot-rawugc-skills--rawugc-api--1x4me3w
---

# RawUGC API

Procedural knowledge for agents to call the RawUGC API. All requests require an API key from the RawUGC dashboard, passed via environment variable.

## Authentication

- **Environment variable**: Read the API key from `RAWUGC_API_KEY`. The key is created in the RawUGC dashboard and must be kept secret; do not hardcode or log it.
- **Header**: Send on every request: `Authorization: Bearer <value of RAWUGC_API_KEY>`.
- If `RAWUGC_API_KEY` is missing or empty, inform the user they must set it and obtain a key from the RawUGC dashboard.

## Base URL

- **Production**: `https://rawugc.com/api/v1`
- All paths below are relative to this base.

## API Versioning

RawUGC uses date-based API versioning. The current latest version is `2026-03-06`.

- **`RawUGC-Version` request header**: Override the version per-request (recommended).
- **API key pinned version**: Set when creating the key in the dashboard.
- **Fallback**: Latest version (`2026-03-06`) if neither is set.

Always send `RawUGC-Version: 2026-03-06` in requests to ensure consistent behavior.

---

## Video Generation

### POST /videos/generate

Initiate video generation.

**Request body (JSON)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `model` | string | Yes | `sora-2-text-to-video`, `sora-2-image-to-video`, `kling-2.6/motion-control`, `veo3`, `veo3_fast` |
| `prompt` | string | For text-to-video / veo3 | Text description (1-5000 chars) |
| `imageUrls` | string[] | For image-to-video / kling | URLs, max 10. Veo3/veo3_fast accept up to 2 optional images. |
| `videoUrls` | string[] | For kling | URLs, max 1. Required for `kling-2.6/motion-control` |
| `aspectRatio` | string | No | Sora: `portrait`/`landscape`. Veo3: `16:9`/`9:16`/`Auto` |
| `nFrames` | string | No | `"10"` or `"15"` (Sora only) |
| `selectedCharacter` | string | No | Character username (e.g. `rawugc.mia`) |
| `characterOrientation` | string | No | `image` or `video` (kling only) |
| `mode` | string | No | `720p` or `1080p` (kling only) |

**Response (201)**: `videoId`, `model`, `status`, `creditsUsed`, `newBalance`, `estimatedCompletionTime`, `createdAt`.

### GET /videos/:videoId

Get video status. Returns `videoId`, `status`, `model`, `prompt`, `creditsUsed`, `url` (when completed), `createdAt`, `completedAt`, `failCode`, `failMessage`, `versions` (edit history array).

### GET /videos

List videos. Query: `status`, `limit` (1-100, default 50), `page`. Returns `videos` array + `pagination`.

### POST /videos/captions

Add styled captions to a completed video. Costs 1 credit.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `videoId` | string | Yes | Video identifier (vid_xxx) |
| `language` | string | No | Language code (e.g. `en`). Defaults to auto-detect |

**Response (200)**: `videoId`, `url`, `version`, `operation`, `creditsUsed`.

### POST /videos/overlay

Add text overlay to a completed video.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `videoId` | string | Yes | Video identifier (vid_xxx) |
| `text` | string | Yes | Overlay text (1-500 chars) |
| `position` | string | No | `top`, `center`, or `bottom` |
| `fontSize` | integer | No | 8-200 pixels |
| `topBottomMargin` | integer | No | 0-500 pixels |
| `strokeThickness` | number | No | 0-10 |

**Response (200)**: `videoId`, `url`, `version`, `operation`, `creditsUsed`.

---

## Image Generation

### POST /images/generate

Generate AI images using Nano Banana models. Async -- poll GET /images/:imageId.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `model` | string | Yes | `nano-banana-2` (text-to-image, 4 credits) or `google/nano-banana-edit` (image editing, 2 credits) |
| `prompt` | string | Yes | Text description or edit instruction (1-20000 chars) |
| `imageUrls` | string[] | For editing | Source images. Required for `google/nano-banana-edit`. Optional for `nano-banana-2` (reference images, max 14). |
| `aspectRatio` | string | No | For `nano-banana-2`: `1:1`, `16:9`, `9:16`, `auto`, etc. |
| `imageSize` | string | No | For `google/nano-banana-edit`: `1:1`, `16:9`, `9:16`, `auto`, etc. |
| `resolution` | string | No | For `nano-banana-2`: `1K`, `2K`, `4K` |
| `outputFormat` | string | No | `png`, `jpeg`, `jpg` |
| `googleSearch` | boolean | No | Use Google Web Search grounding (`nano-banana-2` only) |

**Response (201)**: `imageId`, `model`, `status`, `creditsUsed`, `newBalance`, `estimatedCompletionTime`, `createdAt`.

### GET /images/:imageId

Get image status. Returns `imageId`, `status`, `model`, `prompt`, `url` (when completed), `imageSize`, `resolution`, `outputFormat`, `creditsUsed`, `createdAt`, `completedAt`, `failCode`, `failMessage`.

### GET /images

List images. Query: `status`, `limit` (1-100, default 20), `page`. Returns `images` array + `pagination`.

---

## Music Generation

### POST /music/generate

Generate AI music using Suno models. 3 credits per generation. Async -- poll GET /music/:musicId.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `prompt` | string | Yes | Music description (1-2000 chars) |
| `model` | string | No | `V3_5`, `V4`, `V4_5`, `V4_5PLUS`, `V4_5ALL`, `V5` (default: `V5`) |
| `instrumental` | boolean | No | Instrumental only, no vocals (default: true) |
| `title` | string | No | Track title (max 200 chars). Enables custom mode with `style`. |
| `style` | string | No | Style descriptor (max 500 chars, e.g. `lo-fi hip hop`) |

**Response (201)**: `musicId`, `model`, `status`, `creditsUsed`, `newBalance`, `estimatedCompletionTime`, `createdAt`.

### GET /music/:musicId

Get music status. Returns `musicId`, `status`, `model`, `prompt`, `audioUrl` (when completed), `albumArtUrl`, `duration`, `title`, `creditsUsed`, `createdAt`, `completedAt`, `failCode`, `failMessage`.

### GET /music

List music tracks. Query: `status`, `limit` (1-100, default 20), `page`. Returns `tracks` array + `pagination`.

---

## Upload

### POST /upload

Upload a video or image file. Returns a URL for use in generation requests (`imageUrls`, `videoUrls`) or `analyze-video`. Max 100MB.

**Request**: `multipart/form-data` with `file` field. Accepted types: `video/mp4`, `video/quicktime`, `video/webm`, `image/png`, `image/jpeg`, `image/webp`.

**Response (200)**: `url`, `contentType`, `size`.

---

## Characters

### GET /characters

List all available AI characters (built-in + custom). Returns `characters` array, `count`, `adminCount`, `userCount`.

### GET /characters/:characterId

Get a character by ID. Returns `_id`, `username`, `displayName`, `description`, `videoPreviewUrl`, `type` (`admin`/`user`), `isActive`, `createdAt`, `updatedAt`.

---

## Personas (CRUD)

Personas define target audiences for content plan generation.

- **GET /personas** -- List all. Returns `personas` array + `count`.
- **POST /personas** -- Create. Body: `name` (required, max 200), `description` (required, max 5000). Returns `id`.
- **GET /personas/:personaId** -- Get one.
- **PATCH /personas/:personaId** -- Update. Body: `name`, `description` (both optional).
- **DELETE /personas/:personaId** -- Delete.

**PersonaResponse**: `_id`, `organizationId`, `name`, `description`, `createdAt`, `updatedAt`.

---

## Messaging (CRUD)

Brand/positioning messaging templates.

- **GET /messaging** -- List all. Returns `messages` array + `count`.
- **POST /messaging** -- Create. Body: `name` (required, max 200), `body` (required, max 5000). Returns `id`.
- **GET /messaging/:messageId** -- Get one.
- **PATCH /messaging/:messageId** -- Update. Body: `name`, `body` (both optional).
- **DELETE /messaging/:messageId** -- Delete.

**MessagingResponse**: `_id`, `organizationId`, `name`, `body`, `createdAt`, `updatedAt`.

---

## Products (CRUD)

Products for video generation.

- **GET /products** -- List all. Returns `products` array + `count`.
- **POST /products** -- Create. Body: `name` (required, max 200), `photos` (required, URL array), `description` (max 1000), `messaging` (max 5000). Returns `id`.
- **GET /products/:productId** -- Get one.
- **PATCH /products/:productId** -- Update. Body: `name`, `description`, `photos`, `messaging` (all optional).
- **DELETE /products/:productId** -- Delete.

**ProductResponse**: `_id`, `name`, `description`, `photos`, `messaging`, `createdAt`, `updatedAt`.

---

## Styles (CRUD)

Video/image creative styles with optional prompt templates.

- **GET /styles** -- List all (built-in + custom). Query: `type` (`video`/`image`). Returns `styles` array + `count`.
- **POST /styles** -- Create. Body: `name` (required, max 200), `description` (max 1000), `type` (`video`/`image`), `aspectRatio` (`portrait`/`landscape`/`square`), `promptTemplate` (max 5000, supports `{productName}`, `{messaging}`, `{character}` placeholders). Returns `id`.
- **GET /styles/:styleId** -- Get one.
- **PATCH /styles/:styleId** -- Update. All fields optional.
- **DELETE /styles/:styleId** -- Delete.

**StyleResponse**: `_id`, `name`, `description`, `type`, `aspectRatio`, `styleId`, `promptTemplate`, `isAdmin`, `isStandard`.

---

## Social Scheduling

### GET /social/accounts

List connected social accounts (max 3 per org). Returns `accounts` array + `count`. Each account: `accountId`, `platform` (`tiktok`/`instagram`/`youtube`), `username`, `displayName`, `profilePicture`, `isActive`.

### POST /social/accounts

Sync connected accounts from the scheduling provider. Returns `{ success: boolean }`.

### DELETE /social/accounts/:accountId

Disconnect a social account. Returns `{ success: boolean }`.

### POST /social/posts

Schedule, draft, or immediately publish a video to social media.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `videoUrl` | string | Yes | URL of video to post |
| `accountIds` | string[] | Yes | Target account IDs |
| `mode` | string | Yes | `schedule`, `draft`, or `now` |
| `scheduledFor` | integer | For schedule | Unix timestamp (ms) |
| `timezone` | string | No | IANA timezone (default: `UTC`) |
| `content` | string | No | Caption (max 2200 chars) |
| `videoId` | string | No | RawUGC video ID to link |
| `publishToInbox` | boolean | No | Send to TikTok Creator Inbox |
| `tiktokPrivacyLevel` | string | No | `SELF_ONLY`, `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR` |
| `tiktokAllowComment` | boolean | No | Allow TikTok comments |
| `tiktokAllowDuet` | boolean | No | Allow TikTok duets |
| `tiktokAllowStitch` | boolean | No | Allow TikTok stitches |
| `tiktokCommercialContentType` | string | No | `none`, `brand_organic`, `brand_content` |

**Response (201)**: SocialPost object.

### GET /social/posts

List posts. Query: `fromDate` (ms), `toDate` (ms), `includeDrafts` (boolean). Returns `posts` array + `count`.

### GET /social/posts/:postId

Get a post.

### PATCH /social/posts/:postId

Update a post. Body: `content`, `scheduledFor`, `timezone`, `accountIds` (at least one field required).

### DELETE /social/posts/:postId

Delete a post. Returns `{ success: boolean }`.

### POST /social/posts/:postId/reschedule

Reschedule a post. Body: `scheduledFor` (required, ms), `timezone`.

### POST /social/posts/:postId/publish

Immediately publish a draft post.

**SocialPost**: `postId`, `platforms`, `status` (`draft`/`scheduled`/`published`/`failed`), `scheduledFor`, `timezone`, `content`, `videoUrl`, `createdAt`, `publishedAt`.

---

## Viral Library

### GET /viral-library/videos/:videoId

Get a viral library video with full AI analysis (hooks, keyframes, performance insights). Returns ViralLibraryVideo.

### GET /viral-library/search

Semantic search across analyzed videos. Query: `q` (required, natural language), `limit` (1-50, default 20). Returns `results` (array of `{ video, score }`), `query`, `total`.

---

## Research

### POST /scrape-tiktok

Scrape TikTok videos. Costs 3 credits.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `query` | string | Yes | Search keyword, hashtag, or query (max 500) |
| `mode` | string | No | `keyword`, `hashtag`, `search` (default: `keyword`) |
| `limit` | integer | No | 1-10 (default: 10) |

**Response (200)**: `scrapeId` (use with content-plans), `count`, `videos` (array with `id`, `url`, `author`, `description`, `stats`, `duration`, `hashtags`, `thumbnail`, `videoUrl`).

### POST /content-plans

Generate a content plan from scraped videos. Costs 3 credits.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `scrapeId` | string | Yes | From scrape-tiktok response |
| `brief` | string | Yes | Content plan goals (max 5000) |

**Response (200)**: `planId`, `scrapeId`, `brief`, `topWins`, `gapsToTest`, `blueprints` (array with `category`, `strategy`, `evidence`, `contentIdeas`).

### GET /content-plans

List all content plans. Returns `plans` array + `count`.

### POST /analyze-video

Analyze any video URL (social links or direct URLs). Costs 1 credit. Max 150MB.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `videoUrl` | string | Yes | Video URL to analyze |
| `prompt` | string | No | Custom analysis prompt (max 5000) |

**Response (200)**: `summary`, `hook`, `keyframes` (array with `timestamp`, `type`, `description`, `visual`, `audio`, `text`), `durationSeconds`, `tags`, `whyItPerformed`, `attributesToCopy`, `hooksToTest`.

---

## Errors

All error responses use RFC 7807 Problem Details (JSON): `type`, `title`, `status`, `detail`, `instance`, `errors`.

| Status | Meaning |
|--------|---------|
| 400 | Validation error. Surface `detail` and `errors` to user. |
| 401 | Auth error. Check `RAWUGC_API_KEY`. |
| 402 | Insufficient credits. Add credits in dashboard. |
| 403 | Insufficient scope. API key lacks permissions. |
| 404 | Resource not found. |
| 429 | Rate limit exceeded. Check `X-RateLimit-Reset` header. |
| 500 | Server error. Retry or contact support. |

## Rate Limits

- API Key: 10 req/min. Session: 20 req/min.
- Headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (unix timestamp).

## Workflow: Generate then poll

1. **Generate**: POST to the generation endpoint. Note the returned ID (`videoId`/`imageId`/`musicId`).
2. **Poll**: GET the status endpoint periodically (10-30s). Use exponential backoff.
3. **Finish**: When `status === 'completed'`, use the result URL. When `failed`, surface error to user.
4. **Edit** (video only): POST to `/videos/captions` or `/videos/overlay`.

For full request/response shapes, see [reference.md](reference.md).

---

## Supporting file: reference.md

# RawUGC API -- Request/Response Reference

Condensed shapes for agent lookups. Base URL: `https://rawugc.com/api/v1`. Auth: `Authorization: Bearer <RAWUGC_API_KEY>`. Version: `RawUGC-Version: 2026-03-06`.

---

## Video Generation

### POST /videos/generate

**Request (application/json)**

- `model` (required): `sora-2-text-to-video` | `sora-2-image-to-video` | `kling-2.6/motion-control` | `veo3` | `veo3_fast`
- `prompt` (optional, required for text-to-video/veo3): string, 1-5000 chars
- `imageUrls` (optional, required for image-to-video/kling): string[], max 10. Veo3/veo3_fast accept up to 2.
- `videoUrls` (optional, required for kling): string[], max 1
- `aspectRatio` (optional): Sora: `portrait` | `landscape`. Veo3: `16:9` | `9:16` | `Auto`
- `nFrames` (optional, Sora only): `"10"` | `"15"`
- `selectedCharacter` (optional): string
- `characterOrientation` (optional, kling only): `image` | `video`
- `mode` (optional, kling only): `720p` | `1080p`

**Response 201**: `videoId` (vid_xxx), `model`, `status`, `creditsUsed`, `newBalance`, `estimatedCompletionTime`, `createdAt` (ms epoch)

### GET /videos/{videoId}

**Response 200**: `videoId`, `status` (`pending`|`processing`|`completed`|`failed`), `model`, `prompt`?, `creditsUsed`, `url`? (completed), `createdAt`, `completedAt`?, `failCode`?, `failMessage`?, `versions`? (MediaVersion[])

**MediaVersion**: `videoId`, `version` (int), `url`, `operation`? (`captions`|`overlay`), `createdAt`

### GET /videos

**Query**: `status`?, `limit`? (1-100, default 50), `page`? (default 1)

**Response 200**: `videos` (VideoResponse[]), `pagination` (`total`, `page`, `pageSize`, `hasMore`)

### POST /videos/captions

**Request**: `videoId` (required), `language`? (e.g. `en`)

**Response 200** (EditResponse): `videoId`, `url`, `version`, `operation`, `creditsUsed` (1)

### POST /videos/overlay

**Request**: `videoId` (required), `text` (required, 1-500), `position`? (`top`|`center`|`bottom`), `fontSize`? (8-200), `topBottomMargin`? (0-500), `strokeThickness`? (0-10)

**Response 200** (EditResponse): `videoId`, `url`, `version`, `operation`, `creditsUsed`

---

## Image Generation

### POST /images/generate

**Request**: `model` (required: `nano-banana-2` | `google/nano-banana-edit`), `prompt` (required, 1-20000), `imageUrls`? (required for edit, optional for text-to-image, max 14), `aspectRatio`? (nano-banana-2), `imageSize`? (nano-banana-edit), `resolution`? (`1K`|`2K`|`4K`), `outputFormat`? (`png`|`jpeg`|`jpg`), `googleSearch`? (boolean, nano-banana-2 only)

**Response 201**: `imageId` (img_xxx), `model`, `status`, `creditsUsed`, `newBalance`, `estimatedCompletionTime`, `createdAt`

### GET /images/{imageId}

**Response 200**: `imageId`, `status`, `model`, `prompt`, `url`? (completed), `imageSize`?, `resolution`?, `outputFormat`?, `creditsUsed`, `createdAt`, `completedAt`?, `failCode`?, `failMessage`?

### GET /images

**Query**: `status`?, `limit`? (1-100, default 20), `page`?

**Response 200**: `images` (ImageResponse[]), `pagination` (`total`, `page`, `pageSize`, `hasMore`)

---

## Music Generation

### POST /music/generate

**Request**: `prompt` (required, 1-2000), `model`? (`V3_5`|`V4`|`V4_5`|`V4_5PLUS`|`V4_5ALL`|`V5`, default V5), `instrumental`? (boolean, default true), `title`? (max 200), `style`? (max 500)

**Response 201**: `musicId` (mus_xxx), `model`, `status`, `creditsUsed` (3), `newBalance`, `estimatedCompletionTime`, `createdAt`

### GET /music/{musicId}

**Response 200**: `musicId`, `status`, `model`, `prompt`, `audioUrl`? (completed), `albumArtUrl`?, `duration`?, `title`?, `creditsUsed`, `createdAt`, `completedAt`?, `failCode`?, `failMessage`?

### GET /music

**Query**: `status`?, `limit`? (1-100, default 20), `page`?

**Response 200**: `tracks` (MusicResponse[]), `pagination` (`total`, `page`, `pageSize`, `hasMore`)

---

## Upload

### POST /upload

**Request**: `multipart/form-data`, field `file`. Accepted: `video/mp4`, `video/quicktime`, `video/webm`, `image/png`, `image/jpeg`, `image/webp`. Max 100MB.

**Response 200**: `url` (public URL for use in generation requests), `contentType`, `size` (bytes)

---

## Characters

### GET /characters

**Response 200**: `characters` (CharacterResponse[]), `count`, `adminCount`, `userCount`

### GET /characters/{characterId}

**CharacterResponse**: `_id`, `username`, `displayName`, `description`, `videoPreviewUrl`?, `type` (`admin`|`user`), `isActive`, `createdAt`, `updatedAt`

---

## Personas

### GET /personas

**Response 200**: `personas` (PersonaResponse[]), `count`

### POST /personas

**Request**: `name` (required, max 200), `description` (required, max 5000)

**Response 201**: `{ id }`

### GET /personas/{personaId}

**Response 200**: PersonaResponse

### PATCH /personas/{personaId}

**Request**: `name`?, `description`?

### DELETE /personas/{personaId}

**PersonaResponse**: `_id`, `organizationId`, `name`, `description`, `createdAt`, `updatedAt`

---

## Messaging

### GET /messaging

**Response 200**: `messages` (MessagingResponse[]), `count`

### POST /messaging

**Request**: `name` (required, max 200), `body` (required, max 5000)

**Response 201**: `{ id }`

### GET /messaging/{messageId}

### PATCH /messaging/{messageId}

**Request**: `name`?, `body`?

### DELETE /messaging/{messageId}

**MessagingResponse**: `_id`, `organizationId`, `name`, `body`, `createdAt`, `updatedAt`

---

## Products

### GET /products

**Response 200**: `products` (ProductResponse[]), `count`

### POST /products

**Request**: `name` (required, max 200), `photos` (required, URL[]), `description`? (max 1000), `messaging`? (max 5000)

**Response 201**: `{ id }`

### GET /products/{productId}

### PATCH /products/{productId}

**Request**: `name`?, `description`?, `photos`?, `messaging`?

### DELETE /products/{productId}

**ProductResponse**: `_id`, `name`, `description`, `photos`, `messaging`, `createdAt`, `updatedAt`

---

## Styles

### GET /styles

**Query**: `type`? (`video`|`image`)

**Response 200**: `styles` (StyleResponse[]), `count`

### POST /styles

**Request**: `name` (required, max 200), `description`? (max 1000), `type`? (`video`|`image`), `aspectRatio`? (`portrait`|`landscape`|`square`), `promptTemplate`? (max 5000, placeholders: `{productName}`, `{messaging}`, `{character}`)

**Response 201**: `{ id }`

### GET /styles/{styleId}

### PATCH /styles/{styleId}

**Request**: `name`?, `description`?, `aspectRatio`?, `promptTemplate`?

### DELETE /styles/{styleId}

**StyleResponse**: `_id`, `name`, `description`, `type`, `aspectRatio`, `styleId`, `promptTemplate`, `isAdmin`, `isStandard`

---

## Social Accounts

### GET /social/accounts

**Response 200**: `accounts` (SocialAccount[]), `count`

### POST /social/accounts

Sync accounts from scheduling provider. **Response 200**: `{ success }`

### DELETE /social/accounts/{accountId}

**Response 200**: `{ success }`

**SocialAccount**: `accountId`, `platform` (`tiktok`|`instagram`|`youtube`), `username`, `displayName`, `profilePicture`?, `isActive`

---

## Social Posts

### POST /social/posts

**Request**: `videoUrl` (required), `accountIds` (required, string[]), `mode` (required: `schedule`|`draft`|`now`), `scheduledFor`? (ms, required for schedule), `timezone`? (IANA, default UTC), `content`? (max 2200), `videoId`?, `publishToInbox`?, `tiktokPrivacyLevel`? (`SELF_ONLY`|`PUBLIC_TO_EVERYONE`|`MUTUAL_FOLLOW_FRIENDS`|`FOLLOWER_OF_CREATOR`), `tiktokAllowComment`?, `tiktokAllowDuet`?, `tiktokAllowStitch`?, `tiktokCommercialContentType`? (`none`|`brand_organic`|`brand_content`)

**Response 201**: SocialPost

### GET /social/posts

**Query**: `fromDate`? (ms), `toDate`? (ms), `includeDrafts`? (boolean)

**Response 200**: `posts` (SocialPost[]), `count`

### GET /social/posts/{postId}

### PATCH /social/posts/{postId}

**Request**: `content`?, `scheduledFor`?, `timezone`?, `accountIds`? (at least one field)

### DELETE /social/posts/{postId}

**Response 200**: `{ success }`

### POST /social/posts/{postId}/reschedule

**Request**: `scheduledFor` (required, ms), `timezone`?

### POST /social/posts/{postId}/publish

Immediately publish a draft.

**SocialPost**: `postId`, `platforms`, `status` (`draft`|`scheduled`|`published`|`failed`), `scheduledFor`?, `timezone`, `content`, `videoUrl`, `createdAt`, `publishedAt`?

---

## Viral Library

### GET /viral-library/videos/{videoId}

**Response 200**: ViralLibraryVideo

**ViralLibraryVideo**: `id`, `tiktokId`, `tiktokUrl`, `username`, `description`, `stats` (`views`, `likes`, `comments`, `shares`, `saves`), `hashtags`, `soundName`, `duration`, `thumbnailUrl`, `playUrl`, `analysis`? (VideoAnalysis)

**VideoAnalysis**: `hook`, `summary`, `whyItPerformed`, `attributesToCopy`, `hooksToTest`, `keyframes` (array: `timestamp`, `type`, `description`, `visual`, `audio`, `text`), `durationSeconds`, `tags`, `analyzedAt`

### GET /viral-library/search

**Query**: `q` (required, natural language), `limit`? (1-50, default 20)

**Response 200**: `results` (array of `{ video: ViralLibraryVideo, score: number }`), `query`, `total`

---

## Research

### POST /scrape-tiktok

**Request**: `query` (required, max 500), `mode`? (`keyword`|`hashtag`|`search`, default keyword), `limit`? (1-10, default 10)

**Response 200**: `scrapeId`, `count`, `videos` (array: `id`, `url`, `author`, `description`, `stats` {views, likes, comments, shares}, `duration`, `hashtags`, `thumbnail`?, `videoUrl`?)

### POST /content-plans

**Request**: `scrapeId` (required), `brief` (required, max 5000)

**Response 200**: `planId`, `scrapeId`, `brief`, `topWins`, `gapsToTest`, `blueprints` (array: `category`, `strategy`, `evidence`, `contentIdeas` [{`hook`, `openingShot`, `contentOutline`, `cta`}])

### GET /content-plans

**Response 200**: `plans` (array: `planId`, `scrapeId`, `brief`, `createdAt`), `count`

### POST /analyze-video

**Request**: `videoUrl` (required), `prompt`? (max 5000)

**Response 200**: `summary`, `hook`, `keyframes` (array: `timestamp`, `type`, `description`, `visual`, `audio`, `text`), `durationSeconds`, `tags`, `whyItPerformed`, `attributesToCopy`, `hooksToTest`

---

## Error body (ApiError)

All error responses use RFC 7807: `type`, `title`, `status`, `detail`?, `instance`?, `errors`? (Record<string, string[]>)

**Status codes**: 400 validation, 401 auth, 402 insufficient credits, 403 insufficient scope, 404 not found, 429 rate limit, 500 server error.

## Rate limit headers

All responses include: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (unix timestamp seconds).
