# HumanPost Publishing API

> Upload videos and image carousels, organize them with optional content tags, assign them to social accounts, and track the human posting workflow — via REST or the MCP server. This file is the complete documentation in one place; paste it into your AI tool or fetch it from /llms.txt.

- REST base URL: https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi
- MCP server URL: https://us-central1-clickbaitcarousel.cloudfunctions.net/mcp
- OpenAPI spec: https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/openapi.json
- Version: 2.4.0

## Authentication

Create an API key in **Dashboard → Settings → API & MCP**. Send it with every request as `Authorization: Bearer ccb_live_<key_id>_<secret>`. Treat the key like a password and keep it on your server.

## Quickstart

### 1. List your accounts

Find the account IDs you want HumanPost to publish to.

```bash
curl "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/accounts" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY"
```

### 2. Upload from a URL

Import a public video URL and keep the returned upload ID.

```bash
curl -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/uploads" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind":"video","sourceUrl":"https://cdn.example.com/launch.mp4"}'
```

### 3. Create the post

Queue the processed upload for one or more accounts.

```bash
curl -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/posts" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: launch-post-001" \
  -d '{"media":["UPLOAD_ID"],"accountIds":["ACCOUNT_ID"],"caption":"Launch day 🚀"}'
```

Send a unique `Idempotency-Key` with `POST /v2/posts`. Reusing the same key retries safely without creating a duplicate post.

## Stored account profile state

GET /v2/accounts returns {data:[...]} and GET /v2/accounts/{accountId} returns the same account resource directly. MCP list_accounts / get_account and CLI humanpost accounts list --json / humanpost accounts get ACCOUNT_ID --json expose stored profilePictureUrl, bio, and warmupSearchTerms. Reads require accounts:read and stay scoped to the key's company. Workspace accounts also include these fields under workspace:read. Account creation returns the same resource but still accepts warmup.searchTerms in its request (1–5 terms).

profilePictureUrl reads accounts.profilePicture, falling back to avatarUrl; missing pictures are null. bio reads accounts.bio, returns null when absent, and preserves an explicitly empty string. warmupSearchTerms reads warmupStrategy.searchTerms, with legacy warmup.searchTerms / warmupSearchTerms fallbacks. Missing terms return []; terms are trimmed, empty/non-string entries omitted, and each term limited to 80 characters. Longer historical lists remain readable. Reads do not resolve company defaults.

Setup placeholders contain requested profile values. Pending profile edits retain previous values until worker approval. GIMI can display and compare stored configuration; these reads do not independently verify the live TikTok/Instagram profile or completed warm-up activity. Credentials, recovery data, and private setup fields are excluded.

## Carousels

A carousel is 2–20 images posted as one post. For each local file, create an upload, PUT the raw bytes to its signed URL within 15 minutes using the same Content-Type, and complete the upload. Then create one post with all upload IDs in slide order. A remote image can instead be imported with `{"kind":"image","sourceUrl":"https://..."}`; it completes automatically, so there is no PUT or completion call.

### One carousel with curl

```bash
# 1. Create an upload for slide 1
SIZE=$(wc -c < slide-1.jpg | tr -d ' ')
UPLOAD=$(curl -s -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/uploads" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY" -H "Content-Type: application/json" \
  -d "{\"kind\":\"image\",\"filename\":\"slide-1.jpg\",\"contentType\":\"image/jpeg\",\"sizeBytes\":$SIZE}")
UPLOAD_ID_1=$(printf '%s' "$UPLOAD" | jq -r '.uploadId')
UPLOAD_URL=$(printf '%s' "$UPLOAD" | jq -r '.uploadUrl')

# 2. PUT the raw file, then mark the upload complete
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: image/jpeg" --upload-file slide-1.jpg
curl -s -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/uploads/$UPLOAD_ID_1/complete" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY"

# 3. Repeat for slide 2
SIZE=$(wc -c < slide-2.jpg | tr -d ' ')
UPLOAD=$(curl -s -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/uploads" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY" -H "Content-Type: application/json" \
  -d "{\"kind\":\"image\",\"filename\":\"slide-2.jpg\",\"contentType\":\"image/jpeg\",\"sizeBytes\":$SIZE}")
UPLOAD_ID_2=$(printf '%s' "$UPLOAD" | jq -r '.uploadId')
UPLOAD_URL=$(printf '%s' "$UPLOAD" | jq -r '.uploadUrl')
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: image/jpeg" --upload-file slide-2.jpg
curl -s -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/uploads/$UPLOAD_ID_2/complete" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY"

# 4. Create one post; media array order is slide order
curl -s -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/posts" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: two-slide-launch" \
  -d "{\"media\":[\"$UPLOAD_ID_1\",\"$UPLOAD_ID_2\"],\"accountIds\":[\"ACCOUNT_ID\"],\"caption\":\"Two ideas worth saving\"}"
```

### A folder of carousels

Each subfolder in `./carousels/` becomes one post. Images are sorted by filename and the folder name becomes the caption.

```js
// Node.js 20+; install the CLI first: npm install -g ./cli
// Set HUMANPOST_API_KEY through your shell or secrets manager.
import fs from "node:fs/promises";
import path from "node:path";
import { execFileSync } from "node:child_process";
const env = { ...process.env, HUMANPOST_API_BASE: "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi" };
if (!env.HUMANPOST_API_KEY) throw new Error("Set HUMANPOST_API_KEY first");
const root = "./carousels";
const folders = (await fs.readdir(root, { withFileTypes: true }))
  .filter(entry => entry.isDirectory()).map(entry => entry.name).sort();
function cli(args) {
  return JSON.parse(execFileSync("humanpost", [...args, "--json"],
    { env, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }));
}
for (const folder of folders) {
  const dir = path.join(root, folder);
  const files = (await fs.readdir(dir)).filter(file => /\.(jpe?g|png|webp|heic)$/i.test(file)).sort();
  if (files.length < 2 || files.length > 20) throw new Error(folder + " needs 2–20 images");
  const manifest = path.join(dir, ".upload-manifest.json");
  const state = path.join(dir, ".upload-progress.json");
  await fs.writeFile(manifest, JSON.stringify(files.map(file => path.join(dir, file))));
  // Replays reuse ready upload IDs; changed files/order are rejected by the state check.
  const uploads = cli(["uploads", "local-batch", "--file", manifest, "--state", state]);
  const post = cli(["posts", "create", "--idempotency-key", "carousel-" + folder,
    "--data", JSON.stringify({ media: uploads.results.map(item => item.uploadId),
      accountIds: ["ACCOUNT_ID"], caption: folder })]);
  console.log(folder + ": " + post.postId);
}
```

**Carousel limits:** 2–20 images per carousel; JPG, PNG, WebP, or HEIC; 50 MB maximum per image; The media array sets slide order; Videos are single-file posts.

**Video limits:** One MP4 or MOV file per video post; H.264 or HEVC (H.265) video with AAC audio when audio is present; 200 MB maximum; 3 minutes maximum; Up to 2160 × 3840; 60 fps maximum. Upload completion returns `invalid_media_specs` with a corrective hint when a video fails validation.

## Content tags and template attribution

Content tags organize posts for dashboard analytics and reporting. They are optional, company-scoped metadata and do not appear on the social post. Tags and template provenance are returned together in `contentGroupIds` and appear in the same performance table.

- Tagging is optional. A post can have up to 10 explicit tag or template reporting groups.
- Use the stable contentGroupId returned when a tag is created, not its display name or bare tagId.
- contentGroupIds is a replacement list: resend the complete desired list to add or remove one tag, or send [] to remove all explicit tags.
- Tag assignments can be changed before or after a post is published. Other post fields still follow normal editability rules.
- A template:<id> group inherited from the template used to create a post is provenance and remains visible even when explicit tags are cleared.

### REST workflow

```text
# Create a tag. Save contentGroupId from the response.
POST /v2/content-tags
{ "name": "Launch", "color": "blue" }
→ { "tagId": "abc123", "contentGroupId": "tag:abc123", ... }

# Assign tags while creating a post.
POST /v2/posts
{ "media": ["UPLOAD_ID"], "accountIds": ["ACCOUNT_ID"], "contentGroupIds": ["tag:abc123"] }

# Replace the complete explicit tag list before or after publishing.
PATCH /v2/posts/POST_ID
{ "contentGroupIds": ["tag:abc123", "tag:def456"] }

# Remove every explicit tag.
PATCH /v2/posts/POST_ID
{ "contentGroupIds": [] }
```

List tags with `GET /v2/content-tags`. Rename, recolor, archive, or restore a tag with `PATCH /v2/content-tags/{tagId}`. Delete one permanently with `DELETE /v2/content-tags/{tagId}`; deletion removes that tag from all company posts, so prefer archiving when historical continuity matters. Filter posts with `GET /v2/posts?contentGroupId=tag:abc123`, or use `contentGroupId=untagged`.

## Scheduling posts

Guide: /docs/scheduling

| Window start | Poster local time |
| --- | --- |
| 08:00 | 8 AM – 11 AM |
| 11:00 | 11 AM – 2 PM |
| 14:00 | 2 PM – 5 PM |
| 17:00 | 5 PM – 8 PM |

- For individual posts, scheduledAfter is an ISO earliest-release instant and expiresAt optionally prevents release after that instant. Expiry must be future and after scheduledAfter. Worker-local windows, approval, capacity, and queue order still apply; these fields do not guarantee exact publication or recall an already released task.
- Each selected window adds one scheduled post per day per account, provided approved posts and an available poster are ready. Windows follow each assigned poster's local timezone, not the caller's timezone or UTC.
- The account override takes precedence over the company default. If neither is set, global posting-window and gap rules apply, including the legacy company postsPerDay setting. The new schedule controls cadence through window count; it does not require a separate postsPerDay field.
- Company changes apply only to accounts that inherit the default. Individual account schedules still take priority even if you edit the company default afterward, through the dashboard, API, or MCP. Clearing an account override makes it use the current company default, including any changes made since the override was created. Company scope always means the API key's company, never all companies.
- Replace the complete windows array when enabling or disabling slots. Use 1–4 sorted HH:mm starts, each at least 3 hours apart, from 08:00 through 17:00 inclusive. The four standard dashboard slots are shown above; the shared validator also accepts other starts that meet these constraints.
- Send schedule: null to clear an account override and inherit the company default. Clearing the company schedule restores global rules for inheriting accounts. An empty windows array is invalid. Clearing a schedule does not pause posting.
- Changes affect future dispatches and do not move or cancel tasks already released to posters. Posting windows are delivery windows, not exact publication times. Queue posts as usual with create_post / POST /v2/posts or queue_post / POST /v2/posts/{postId}/queue.
- Missed posts may catch up or use unused windows as make-up slots. Four posts per account per poster-local day remains the hard maximum, including make-up posts. Selecting fewer slots sets the normal cadence, not an absolute cap on make-up posts.
- Read schedules with accounts:read and update them with accounts:write. Account IDs must belong to the API key's company. Reads return source (account, company, or global), scheduledPostsPerDay (null for global rules), and maxPostsPerDay. Account reads also return the company default, effective schedule, and resolved timezone; before poster assignment the reported timezone is a fallback.

### REST scheduling

```bash
# Read the company default
curl "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/schedule" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY"

# Two daily windows for accounts using the company default
curl -X PATCH "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/schedule" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"schedule":{"windows":["08:00","17:00"]}}'

# Override one account with a single midday window
curl -X PATCH "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/accounts/ACCOUNT_ID/schedule" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"schedule":{"windows":["11:00"]}}'

# Restore company inheritance for that account
curl -X PATCH "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/accounts/ACCOUNT_ID/schedule" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"schedule":null}'

# Verify the effective schedule and its source
curl "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/accounts/ACCOUNT_ID/schedule" \
  -H "Authorization: Bearer $HUMANPOST_API_KEY"
```

### MCP scheduling

```javascript
get_company_schedule({})
update_company_schedule({"schedule":{"windows":["08:00","17:00"]}})
get_account_schedule({"accountId":"ACCOUNT_ID"})
update_account_schedule({"accountId":"ACCOUNT_ID","schedule":{"windows":["11:00"]}})
// Clear the override to inherit the company default again
update_account_schedule({"accountId":"ACCOUNT_ID","schedule":null})
```

## Post status lifecycle

| Status | Meaning |
| --- | --- |
| `draft` | The post exists but has not been queued to any accounts. |
| `pending_approval` | The post is waiting for a HumanPost reviewer. |
| `queued` | Approved and waiting in each target account's queue. |
| `posting` | A human operator is actively publishing the post. |
| `posted` | Published successfully to the social platform. |
| `partially_posted` | Published to some accounts while other account targets remain active. |
| `processing` | Uploaded media is still being prepared. |
| `cancelled` | The queued item was cancelled before posting. |
| `failed` | Publishing or media processing could not complete. |

## Errors

Every failed request uses this JSON shape. The `hint` explains how to fix the problem; include `requestId` if you contact support.

```json
{
  "error": {
    "code": "invalid_request",
    "message": "accountIds must contain at least one account",
    "hint": "pass one or more IDs returned by GET /v2/accounts",
    "requestId": "req_01J...",
    "retryable": false
  }
}
```

| Error code | Meaning |
| --- | --- |
| `unauthorized` | The API key is missing, malformed, revoked, or invalid. |
| `insufficient_scope` | The key does not include the required scope. |
| `idempotency_conflict` | The key was already used for a different operation or payload. |
| `version_conflict` | The draft changed since it was read or previewed. |
| `analytics_unavailable` | V3 rollups are not ready for a complete report. |
| `job_not_found` | The job is missing or belongs to another company. |
| `invalid_request` | The body, query, or path values failed validation. |
| `account_not_found` | The account does not exist or belongs to another company. |
| `upload_not_ready` | The upload has not finished processing. |
| `media_not_ready` | The existing post has no ready video or current rendered carousel media. |
| `invalid_media_set` | The media combination cannot form a supported post. |
| `instagram_feed_posts_disabled` | The company is not permitted to publish regular Instagram feed posts; use a Reel. |
| `duplicate_queue_item` | The same post is already queued for an account. |
| `post_not_editable` | The post has advanced beyond an editable status. |
| `post_not_cancellable` | No targeted item can be cancelled; released items may be in an operator's hands, and posted items cannot be deleted. |
| `account_limit_reached` | The company has used every account slot allowed by its plan. |
| `rate_limited` | A per-company UTC daily API, post, upload, or account-creation cap has been reached. |
| `quota_exceeded` | The company has reached a plan or posting limit. |
| `file_too_large` | The uploaded file exceeds the supported size. |
| `unsupported_media_type` | The file type or content type is not supported. |
| `invalid_media_specs` | The video failed codec, duration, dimension, or frame-rate validation. |
| `invalid_cursor` | The value used to get the next page is malformed or expired. |

## Getting more results

List endpoints return up to 100 items at a time. When there are more, the response includes a `nextCursor` value. Send it back on the next request as `?cursor=` to get the next page; never change or interpret it. When `nextCursor` is `null`, you have everything.

```text
# First page
GET /v2/posts?limit=25
→ { "data": [ 25 posts ], "nextCursor": "eyJj..." }

# Next page — pass nextCursor back exactly as received
GET /v2/posts?limit=25&cursor=eyJj...
→ { "data": [ 12 posts ], "nextCursor": null }   # null = no more pages
```

## Scopes

A key can only do what its scopes allow. Pick scopes when you create the key in the dashboard.

| Scope | What it allows |
| --- | --- |
| `tasks:read` | Discover task types and read company task progress and history. |
| `tasks:write` | Create Comment or Amplify tasks, cancel remaining work, and resume blocked Amplify components. |
| `tasks:evidence:read` | Read company task screenshots through expiring download links. |
| `posts:read` | List posts and content tags, inspect status, and read account queues. |
| `posts:write` | Upload media; create, queue, edit, or cancel posts; and manage content tags. |
| `accounts:read` | List publishing accounts and read company or account posting schedules. |
| `accounts:write` | Create publishing accounts and change company or account posting schedules. |
| `analytics:read` | Read post and account performance analytics. |
| `usage:read` | View plan limits and daily API usage. |
| `workspace:read` | Read the key's company, connected accounts, image folders, and image-pack metadata. |
| `media:read` | Create short-lived download URLs for selected company-owned images. |

## Drafts, jobs, and CLI

Create an unqueued draft with create_draft, inspect get_post_content, edit update_draft with expectedVersionId, request_post_preview, poll get_job, inspect get_post_preview, then queue_post with an idempotencyKey. Artwork changes invalidate the preview. Upload batches accept 1–100 public URL items; results retain input order and per-file errors. Analytics reports require V3 rollups and UTC-midnight from/to dates (end exclusive, maximum 366 days). Growth follows collection dates; top posts use latest cumulative views. Coverage and freshness are included. Tag groups overlap.

Use scheduledAfter and optional expiresAt on create_post / queue_post to set an earliest release and latest allowed release. These are ISO timestamps; worker-local windows and capacity still govern dispatch. Released tasks cannot be recalled by expiry.

CLI installation from this repository: npm install -g ./cli. Set HUMANPOST_API_KEY, then humanpost doctor, humanpost context, humanpost posts list --all --json. Use humanpost drafts create --file draft.json --idempotency-key campaign-draft, humanpost posts render POST_ID --wait, then humanpost posts queue POST_ID --file targets.json --idempotency-key campaign-submit. Local media: humanpost uploads local FILE. Resume local batches: humanpost uploads local-batch --file manifest.json --state progress.json. See /docs/cli. Authentication keys never appear in CLI arguments; use environment or auth login --key-stdin. JSON goes to stdout and errors to stderr.

Use COMMAND --help for a concise field reference and example, or COMMAND --schema for full nested constraints. Named profiles require --profile NAME on every command; environment credentials take precedence. Replace upload, post, version, and account placeholders with returned IDs. A queued post may still need managed approval. Inspect preview status: ready and mediaUrls before submitting. A terminal timeout leaves durable server work running.

### Comment and Amplify tasks

Choose the Tasks API-key preset. Use tasks:read for capability discovery, list/detail/history; tasks:write for create/cancel/resume; tasks:evidence:read for temporary screenshots; accounts:read to select company accounts; usage:read for Comment/Amplify credit balances. Keys belong on partner servers. Existing keys do not gain new scopes automatically.

POST /v2/tasks requires Idempotency-Key and a body like {"type":"amplify_video","accountId":"ACCOUNT_ID","externalReference":"partner-order-123","payload":{"targetVideoUrl":"https://www.tiktok.com/@creator/video/123456","commentText":"hi"}}. Create Comment with type comment_video. Optional allowAccountRollover defaults false; optional TikTok commentImageUrl is supported. Amplify reserves one Amplify credit and keeps Repost + Like and Comment components under one public taskId. Standalone Comment reserves one Comment credit. Business hours and worker availability apply; older blank-task-capable apps work. Creation retries must use the original payload and key.

GET /v2/task-types?accountId=ACCOUNT_ID discovers dispatch availability. GET /v2/tasks and /v2/tasks/{taskId} expose company tasks; the original ID follows comment replacements. Only completed means HumanPost-approved; pending_approval awaits review. GET /events gives asynchronous revision history (deduplicate eventId), and GET /evidence returns signed links expiring after 900 seconds by default (60–3600 supported). Follow nextCursor even after an empty filtered/evidence page. Legacy historyComplete:false means older events were not reconstructed. Poll from the first page for new events; no webhooks are delivered.

Use availableActions to render controls. POST /cancel takes a reason, preserves history, and refunds eligible work once; Amplify refunds only if neither component was approved. Old monthly credits do not replenish a new period. Cancellation cannot undo platform actions. POST /resume reopens blocked Amplify work on its original account/worker, preserving approved work without another charge. Worker assignment, review and payouts remain internal. The list/read interface supports known company task types; creation/control here support Comment and Amplify, with publishing handled through post endpoints.

MCP tools: list_task_types, create_task, list_tasks, get_task, list_task_events, get_task_evidence, cancel_task, resume_task. create_task takes {request:<task-body>,idempotencyKey:"partner-order-123"}; other tools take taskId where relevant. CLI: humanpost tasks types --account-id ACCOUNT_ID; tasks create --file task.json --idempotency-key partner-order-123; tasks list --all; tasks get TASK_ID --wait; tasks events TASK_ID --all; tasks evidence TASK_ID --all; tasks cancel TASK_ID --data '{"reason":"Customer cancelled"}'; tasks resume TASK_ID. --wait timeout retains task state and does not cancel server work.

### CLI payload examples

#### files.json

```json
[
  "./slide1.png",
  "./slide2.png"
]
```

#### draft.json

```json
{
  "media": [
    "UPLOAD_1",
    "UPLOAD_2"
  ],
  "caption": "Our launch is here"
}
```

#### edit.json

```json
{
  "expectedVersionId": "VERSION_ID",
  "slides": [
    {
      "image": "UPLOAD_1",
      "textElements": [
        {
          "content": "Launch day",
          "style": {
            "fontSize": 48,
            "positionY": 20
          }
        }
      ]
    },
    {
      "image": "UPLOAD_2",
      "textElements": []
    }
  ]
}
```

#### targets.json

```json
{
  "accountIds": [
    "ACCOUNT_ID"
  ]
}
```

#### report.json

```json
{
  "from": "2026-09-01T00:00:00Z",
  "to": "2026-10-01T00:00:00Z",
  "groupBy": "account",
  "comparePrevious": true,
  "topLimit": 10
}
```

#### urls.json

```json
{
  "items": [
    {
      "kind": "image",
      "sourceUrl": "https://example.com/slide.jpg"
    }
  ]
}
```

#### task.json

```json
{
  "type": "amplify_video",
  "accountId": "ACCOUNT_ID",
  "externalReference": "partner-order-123",
  "payload": {
    "targetVideoUrl": "https://www.tiktok.com/@creator/video/123456",
    "commentText": "hi"
  }
}
```

#### cancellation.json

```json
{
  "reason": "Customer cancelled the remaining work"
}
```

### CLI recovery

- Private file permissions (exit 3): On POSIX, use chmod 700 on the HumanPost config directory and chmod 600 on config.json and local upload state files. Files must belong to you and must not be symbolic links. State files contain temporary signed upload URLs; keep them private.
- invalid_media_specs (exit 2): Upload a valid image whose bytes match its MIME type: one image, at most 40 megapixels. Convert unsupported HEIC files to JPG. Videos must meet the technical requirements shown in the error hint.
- rate_limit_unavailable (exit 1): HumanPost temporarily cannot verify request limits, so the operation stops before running. Retry shortly using the original payload and idempotency key.
- Authentication or scope error (exit 3): Run humanpost auth status and humanpost doctor. Pass --profile NAME on every command that uses a named profile. Create a key with the required scopes if access is missing.
- idempotency_conflict (exit 4): Retry the same operation with its original payload and key. For a new or changed operation, choose a new key.
- version_conflict (exit 4): Run humanpost posts content POST_ID, merge your edit into the current slides, and use the returned versionId as expectedVersionId.
- Upload interrupted (exit 1): Rerun local-batch with the same manifest, files, server, and --state path. Ready upload IDs are reused.
- Job still running (exit 1): Use the returned jobId with humanpost jobs get JOB_ID --wait. A terminal timeout does not cancel server work.
- Job partially failed (exit 1): Use --json to inspect error.details.job.results. Reuse successful upload IDs and submit only failed items as a new batch with a new key.
- analytics_unavailable (exit 1): Complete the analytics V3 backfill before retrying. Reports require ready rollups; absent observations do not mean zero performance.

## MCP field reference

### list_task_types

Discover comment and Amplify inputs, credit costs, availability and blockers. Optionally inspect a company account. Worker business hours apply; review remains with HumanPost. Requires tasks:read.

**Input**
- `accountId`: string [minLength=1, maxLength=200]

**Output**
- `data`: array<object> (required)
  - `type`: comment_video | amplify_video (required)
  - `platforms`: array<string> (required)
  - `requiredPayloadFields`: array<string> (required)
  - `creditKind`: string (required)
  - `creditQuantity`: number (required)
  - `evidence`: string (required)
  - `available`: boolean (required)
  - `blockers`: array<string> (required)
  - `actions`: array<string> (required)
- `scheduling`: worker_business_hours (required)
- `review`: humanpost (required)
- `accountId`: string | null (required)

### create_task

Create an exact Comment or one Amplify request containing Repost + Like and Comment. Requires idempotencyKey; keep payload unchanged for retries. No worker or payout overrides. Creation reserves one credit. Requires tasks:write.

**Input**
- `request`: object or object (required)
  - Variant 1:
    - `accountId`: string (required) [minLength=1, maxLength=200]
    - `externalReference`: string [minLength=1, maxLength=200]
    - `type`: comment_video (required)
    - `payload`: object (required)
      - `targetVideoUrl`: string (required) [maxLength=2000, format="uri"]
      - `commentText`: string (required) [minLength=1, maxLength=500]
      - `commentImageUrl`: string [maxLength=2000, format="uri"]
      - `allowAccountRollover`: boolean [default=false]
  - Variant 2:
    - `accountId`: string (required) [minLength=1, maxLength=200]
    - `externalReference`: string [minLength=1, maxLength=200]
    - `type`: amplify_video (required)
    - `payload`: object (required)
      - `targetVideoUrl`: string (required) [maxLength=2000, format="uri"]
      - `commentText`: string (required) [minLength=1, maxLength=500]
- `idempotencyKey`: string (required) [minLength=1, maxLength=200]

**Output**
- `taskId`: string (required)
- `companyId`: string (required)
- `type`: post_video | amplify_video | repost_like_video | like_video | comment_video | reply_to_comment | like_comment | follow_account | edit_bio | edit_username | edit_display_name | update_profile_pic | account_login | account_logout | account_setup | warmup_scroll | record_fyp | record_reels | amazon_review | g2_review | trustpilot_review | intro_video | spark_code | connect_posting_service | delete_post | delete_account | blank_task (required)
- `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
- `accountId`: string | null (required)
- `currentAccountId`: string | null (required)
- `platform`: string | null (required)
- `externalReference`: string | null (required)
- `payload`: object (required)
  - `targetVideoUrl`: string
  - `commentText`: string
  - `commentImageAvailable`: boolean
  - `allowAccountRollover`: boolean
  - `replyText`: string
  - `directCommentUrl`: string
  - `commentAuthor`: string
  - `tiktokUrl`: string
  - `title`: string
  - `bio`: string
  - `username`: string
  - `displayName`: string
  - `newBio`: string
  - `newUsername`: string
  - `newDisplayName`: string
  - `videoId`: string
  - `profilePictureAvailable`: boolean
- `createdAt`: string | null (required) [format="date-time"]
- `updatedAt`: string | null (required) [format="date-time"]
- `completedAt`: string | null (required) [format="date-time"]
- `reason`: string | null (required)
- `components`: array<object> (required)
  - `taskId`: string (required)
  - `role`: string (required)
  - `accountId`: string | null (required)
  - `username`: string | null (required)
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `scheduledFor`: string | null (required) [format="date-time"]
  - `deadline`: string | null (required) [format="date-time"]
  - `startedAt`: string | null (required) [format="date-time"]
  - `submittedAt`: string | null (required) [format="date-time"]
  - `reviewedAt`: string | null (required) [format="date-time"]
  - `completedAt`: string | null (required) [format="date-time"]
  - `reason`: string | null (required)
  - `evidenceAvailable`: boolean (required)
- `attempts`: array<object> (required)
  - `taskId`: string (required)
  - `role`: string (required)
  - `accountId`: string | null (required)
  - `username`: string | null (required)
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `scheduledFor`: string | null (required) [format="date-time"]
  - `deadline`: string | null (required) [format="date-time"]
  - `startedAt`: string | null (required) [format="date-time"]
  - `submittedAt`: string | null (required) [format="date-time"]
  - `reviewedAt`: string | null (required) [format="date-time"]
  - `completedAt`: string | null (required) [format="date-time"]
  - `reason`: string | null (required)
  - `evidenceAvailable`: boolean (required)
- `currentAttemptId`: string (required)
- `availableActions`: array<cancel | resume> (required)
- `evidenceAvailable`: boolean (required)
- `credit`: object (required)
  - `kind`: string | null (required)
  - `quantity`: number (required)
  - `source`: string | null (required)
  - `outcome`: reserved | consumed | refunded | not_charged (required)
- `historyComplete`: boolean (required)

### list_tasks

List company-visible requests and effective status. Operational Amplify children and comment replacements are embedded in the original task. Follow nextCursor even after an empty filtered page. Requires tasks:read.

**Input**
- `limit`: integer [minimum=1, maximum=100, default=25]
- `cursor`: string [maxLength=2000]
- `type`: post_video | amplify_video | repost_like_video | like_video | comment_video | reply_to_comment | like_comment | follow_account | edit_bio | edit_username | edit_display_name | update_profile_pic | account_login | account_logout | account_setup | warmup_scroll | record_fyp | record_reels | amazon_review | g2_review | trustpilot_review | intro_video | spark_code | connect_posting_service | delete_post | delete_account | blank_task
- `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown
- `accountId`: string [minLength=1, maxLength=200]
- `platform`: tiktok | instagram
- `externalReference`: string [maxLength=200]
- `createdAfter`: string [format="date-time"]
- `createdBefore`: string [format="date-time"]

**Output**
- `data`: array<object> (required)
  - `taskId`: string (required)
  - `companyId`: string (required)
  - `type`: post_video | amplify_video | repost_like_video | like_video | comment_video | reply_to_comment | like_comment | follow_account | edit_bio | edit_username | edit_display_name | update_profile_pic | account_login | account_logout | account_setup | warmup_scroll | record_fyp | record_reels | amazon_review | g2_review | trustpilot_review | intro_video | spark_code | connect_posting_service | delete_post | delete_account | blank_task (required)
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `accountId`: string | null (required)
  - `currentAccountId`: string | null (required)
  - `platform`: string | null (required)
  - `externalReference`: string | null (required)
  - `payload`: object (required)
    - `targetVideoUrl`: string
    - `commentText`: string
    - `commentImageAvailable`: boolean
    - `allowAccountRollover`: boolean
    - `replyText`: string
    - `directCommentUrl`: string
    - `commentAuthor`: string
    - `tiktokUrl`: string
    - `title`: string
    - `bio`: string
    - `username`: string
    - `displayName`: string
    - `newBio`: string
    - `newUsername`: string
    - `newDisplayName`: string
    - `videoId`: string
    - `profilePictureAvailable`: boolean
  - `createdAt`: string | null (required) [format="date-time"]
  - `updatedAt`: string | null (required) [format="date-time"]
  - `completedAt`: string | null (required) [format="date-time"]
  - `reason`: string | null (required)
  - `components`: array<object> (required)
    - `taskId`: string (required)
    - `role`: string (required)
    - `accountId`: string | null (required)
    - `username`: string | null (required)
    - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
    - `scheduledFor`: string | null (required) [format="date-time"]
    - `deadline`: string | null (required) [format="date-time"]
    - `startedAt`: string | null (required) [format="date-time"]
    - `submittedAt`: string | null (required) [format="date-time"]
    - `reviewedAt`: string | null (required) [format="date-time"]
    - `completedAt`: string | null (required) [format="date-time"]
    - `reason`: string | null (required)
    - `evidenceAvailable`: boolean (required)
  - `attempts`: array<object> (required)
    - `taskId`: string (required)
    - `role`: string (required)
    - `accountId`: string | null (required)
    - `username`: string | null (required)
    - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
    - `scheduledFor`: string | null (required) [format="date-time"]
    - `deadline`: string | null (required) [format="date-time"]
    - `startedAt`: string | null (required) [format="date-time"]
    - `submittedAt`: string | null (required) [format="date-time"]
    - `reviewedAt`: string | null (required) [format="date-time"]
    - `completedAt`: string | null (required) [format="date-time"]
    - `reason`: string | null (required)
    - `evidenceAvailable`: boolean (required)
  - `currentAttemptId`: string (required)
  - `availableActions`: array<cancel | resume> (required)
  - `evidenceAvailable`: boolean (required)
  - `credit`: object (required)
    - `kind`: string | null (required)
    - `quantity`: number (required)
    - `source`: string | null (required)
    - `outcome`: reserved | consumed | refunded | not_charged (required)
  - `historyComplete`: boolean (required)
- `nextCursor`: string | null (required)

### get_task

Read combined Amplify progress or the full bounded comment attempt chain under one stable task ID. Completed means approved; submitted screenshots await review. Requires tasks:read.

**Input**
- `taskId`: string (required) [minLength=1, maxLength=200]

**Output**
- `taskId`: string (required)
- `companyId`: string (required)
- `type`: post_video | amplify_video | repost_like_video | like_video | comment_video | reply_to_comment | like_comment | follow_account | edit_bio | edit_username | edit_display_name | update_profile_pic | account_login | account_logout | account_setup | warmup_scroll | record_fyp | record_reels | amazon_review | g2_review | trustpilot_review | intro_video | spark_code | connect_posting_service | delete_post | delete_account | blank_task (required)
- `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
- `accountId`: string | null (required)
- `currentAccountId`: string | null (required)
- `platform`: string | null (required)
- `externalReference`: string | null (required)
- `payload`: object (required)
  - `targetVideoUrl`: string
  - `commentText`: string
  - `commentImageAvailable`: boolean
  - `allowAccountRollover`: boolean
  - `replyText`: string
  - `directCommentUrl`: string
  - `commentAuthor`: string
  - `tiktokUrl`: string
  - `title`: string
  - `bio`: string
  - `username`: string
  - `displayName`: string
  - `newBio`: string
  - `newUsername`: string
  - `newDisplayName`: string
  - `videoId`: string
  - `profilePictureAvailable`: boolean
- `createdAt`: string | null (required) [format="date-time"]
- `updatedAt`: string | null (required) [format="date-time"]
- `completedAt`: string | null (required) [format="date-time"]
- `reason`: string | null (required)
- `components`: array<object> (required)
  - `taskId`: string (required)
  - `role`: string (required)
  - `accountId`: string | null (required)
  - `username`: string | null (required)
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `scheduledFor`: string | null (required) [format="date-time"]
  - `deadline`: string | null (required) [format="date-time"]
  - `startedAt`: string | null (required) [format="date-time"]
  - `submittedAt`: string | null (required) [format="date-time"]
  - `reviewedAt`: string | null (required) [format="date-time"]
  - `completedAt`: string | null (required) [format="date-time"]
  - `reason`: string | null (required)
  - `evidenceAvailable`: boolean (required)
- `attempts`: array<object> (required)
  - `taskId`: string (required)
  - `role`: string (required)
  - `accountId`: string | null (required)
  - `username`: string | null (required)
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `scheduledFor`: string | null (required) [format="date-time"]
  - `deadline`: string | null (required) [format="date-time"]
  - `startedAt`: string | null (required) [format="date-time"]
  - `submittedAt`: string | null (required) [format="date-time"]
  - `reviewedAt`: string | null (required) [format="date-time"]
  - `completedAt`: string | null (required) [format="date-time"]
  - `reason`: string | null (required)
  - `evidenceAvailable`: boolean (required)
- `currentAttemptId`: string (required)
- `availableActions`: array<cancel | resume> (required)
- `evidenceAvailable`: boolean (required)
- `credit`: object (required)
  - `kind`: string | null (required)
  - `quantity`: number (required)
  - `source`: string | null (required)
  - `outcome`: reserved | consumed | refunded | not_charged (required)
- `historyComplete`: boolean (required)

### list_task_events

Page persisted task revisions; events arrive asynchronously. historyComplete is false for legacy tasks; missing history is not invented. Requires tasks:read.

**Input**
- `limit`: integer [minimum=1, maximum=100, default=25]
- `cursor`: string [maxLength=2000]
- `taskId`: string (required) [minLength=1, maxLength=200]

**Output**
- `data`: array<object> (required)
  - `eventId`: string (required)
  - `taskId`: string (required)
  - `operationalTaskId`: string (required)
  - `role`: string (required)
  - `occurredAt`: string (required) [format="date-time"]
  - `revision`: string (required)
  - `kind`: created | updated | deleted (required)
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `reason`: string | null (required)
  - `scheduledFor`: string | null (required) [format="date-time"]
  - `submittedAt`: string | null (required) [format="date-time"]
  - `reviewedAt`: string | null (required) [format="date-time"]
  - `creditOutcome`: string (required)
- `nextCursor`: string | null (required)
- `historyComplete`: boolean (required)

### get_task_evidence

Page current and archived screenshots with expiring signed URLs. Requires company task ownership. URLs are bearer links until expiry; no worker personal data is included. Requires tasks:evidence:read.

**Input**
- `limit`: integer [minimum=1, maximum=100, default=25]
- `cursor`: string [maxLength=2000]
- `expiresInSeconds`: integer [minimum=60, maximum=3600, default=900]
- `taskId`: string (required) [minLength=1, maxLength=200]

**Output**
- `data`: array<object> (required)
  - `evidenceId`: string (required)
  - `taskId`: string (required)
  - `operationalTaskId`: string (required)
  - `role`: string (required)
  - `url`: string (required) [format="uri"]
  - `expiresAt`: string (required) [format="date-time"]
  - `submittedAt`: string | null (required) [format="date-time"]
  - `reviewedAt`: string | null (required) [format="date-time"]
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `historical`: boolean (required)
- `nextCursor`: string | null (required)
- `expiresAt`: string (required) [format="date-time"]

### cancel_task

Cancel unfinished Comment or Amplify work while retaining evidence/history. Cancellation cannot undo platform actions. Amplify refunds only before either component is approved. Requires tasks:write.

**Input**
- `reason`: string (required) [minLength=1, maxLength=500]
- `taskId`: string (required) [minLength=1, maxLength=200]

**Output**
- `taskId`: string (required)
- `companyId`: string (required)
- `type`: post_video | amplify_video | repost_like_video | like_video | comment_video | reply_to_comment | like_comment | follow_account | edit_bio | edit_username | edit_display_name | update_profile_pic | account_login | account_logout | account_setup | warmup_scroll | record_fyp | record_reels | amazon_review | g2_review | trustpilot_review | intro_video | spark_code | connect_posting_service | delete_post | delete_account | blank_task (required)
- `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
- `accountId`: string | null (required)
- `currentAccountId`: string | null (required)
- `platform`: string | null (required)
- `externalReference`: string | null (required)
- `payload`: object (required)
  - `targetVideoUrl`: string
  - `commentText`: string
  - `commentImageAvailable`: boolean
  - `allowAccountRollover`: boolean
  - `replyText`: string
  - `directCommentUrl`: string
  - `commentAuthor`: string
  - `tiktokUrl`: string
  - `title`: string
  - `bio`: string
  - `username`: string
  - `displayName`: string
  - `newBio`: string
  - `newUsername`: string
  - `newDisplayName`: string
  - `videoId`: string
  - `profilePictureAvailable`: boolean
- `createdAt`: string | null (required) [format="date-time"]
- `updatedAt`: string | null (required) [format="date-time"]
- `completedAt`: string | null (required) [format="date-time"]
- `reason`: string | null (required)
- `components`: array<object> (required)
  - `taskId`: string (required)
  - `role`: string (required)
  - `accountId`: string | null (required)
  - `username`: string | null (required)
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `scheduledFor`: string | null (required) [format="date-time"]
  - `deadline`: string | null (required) [format="date-time"]
  - `startedAt`: string | null (required) [format="date-time"]
  - `submittedAt`: string | null (required) [format="date-time"]
  - `reviewedAt`: string | null (required) [format="date-time"]
  - `completedAt`: string | null (required) [format="date-time"]
  - `reason`: string | null (required)
  - `evidenceAvailable`: boolean (required)
- `attempts`: array<object> (required)
  - `taskId`: string (required)
  - `role`: string (required)
  - `accountId`: string | null (required)
  - `username`: string | null (required)
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `scheduledFor`: string | null (required) [format="date-time"]
  - `deadline`: string | null (required) [format="date-time"]
  - `startedAt`: string | null (required) [format="date-time"]
  - `submittedAt`: string | null (required) [format="date-time"]
  - `reviewedAt`: string | null (required) [format="date-time"]
  - `completedAt`: string | null (required) [format="date-time"]
  - `reason`: string | null (required)
  - `evidenceAvailable`: boolean (required)
- `currentAttemptId`: string (required)
- `availableActions`: array<cancel | resume> (required)
- `evidenceAvailable`: boolean (required)
- `credit`: object (required)
  - `kind`: string | null (required)
  - `quantity`: number (required)
  - `source`: string | null (required)
  - `outcome`: reserved | consumed | refunded | not_charged (required)
- `historyComplete`: boolean (required)

### resume_task

Resume failed/expired components of an unfinished Amplify bundle on the original account and worker. Approved components stay complete; no additional credit is charged. Requires tasks:write.

**Input**
- `taskId`: string (required) [minLength=1, maxLength=200]

**Output**
- `taskId`: string (required)
- `companyId`: string (required)
- `type`: post_video | amplify_video | repost_like_video | like_video | comment_video | reply_to_comment | like_comment | follow_account | edit_bio | edit_username | edit_display_name | update_profile_pic | account_login | account_logout | account_setup | warmup_scroll | record_fyp | record_reels | amazon_review | g2_review | trustpilot_review | intro_video | spark_code | connect_posting_service | delete_post | delete_account | blank_task (required)
- `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
- `accountId`: string | null (required)
- `currentAccountId`: string | null (required)
- `platform`: string | null (required)
- `externalReference`: string | null (required)
- `payload`: object (required)
  - `targetVideoUrl`: string
  - `commentText`: string
  - `commentImageAvailable`: boolean
  - `allowAccountRollover`: boolean
  - `replyText`: string
  - `directCommentUrl`: string
  - `commentAuthor`: string
  - `tiktokUrl`: string
  - `title`: string
  - `bio`: string
  - `username`: string
  - `displayName`: string
  - `newBio`: string
  - `newUsername`: string
  - `newDisplayName`: string
  - `videoId`: string
  - `profilePictureAvailable`: boolean
- `createdAt`: string | null (required) [format="date-time"]
- `updatedAt`: string | null (required) [format="date-time"]
- `completedAt`: string | null (required) [format="date-time"]
- `reason`: string | null (required)
- `components`: array<object> (required)
  - `taskId`: string (required)
  - `role`: string (required)
  - `accountId`: string | null (required)
  - `username`: string | null (required)
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `scheduledFor`: string | null (required) [format="date-time"]
  - `deadline`: string | null (required) [format="date-time"]
  - `startedAt`: string | null (required) [format="date-time"]
  - `submittedAt`: string | null (required) [format="date-time"]
  - `reviewedAt`: string | null (required) [format="date-time"]
  - `completedAt`: string | null (required) [format="date-time"]
  - `reason`: string | null (required)
  - `evidenceAvailable`: boolean (required)
- `attempts`: array<object> (required)
  - `taskId`: string (required)
  - `role`: string (required)
  - `accountId`: string | null (required)
  - `username`: string | null (required)
  - `status`: pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown (required)
  - `scheduledFor`: string | null (required) [format="date-time"]
  - `deadline`: string | null (required) [format="date-time"]
  - `startedAt`: string | null (required) [format="date-time"]
  - `submittedAt`: string | null (required) [format="date-time"]
  - `reviewedAt`: string | null (required) [format="date-time"]
  - `completedAt`: string | null (required) [format="date-time"]
  - `reason`: string | null (required)
  - `evidenceAvailable`: boolean (required)
- `currentAttemptId`: string (required)
- `availableActions`: array<cancel | resume> (required)
- `evidenceAvailable`: boolean (required)
- `credit`: object (required)
  - `kind`: string | null (required)
  - `quantity`: number (required)
  - `source`: string | null (required)
  - `outcome`: reserved | consumed | refunded | not_charged (required)
- `historyComplete`: boolean (required)

### get_context

Call first to confirm the key's company, scopes, available tools, and publishing capabilities.

**Input**


**Output**
- `company`: object (required)
  - `companyId`: string (required)
  - `name`: string (required)
- `scopes`: array<posts:read | posts:write | accounts:read | accounts:write | analytics:read | usage:read | workspace:read | media:read | tasks:read | tasks:write | tasks:evidence:read> (required)
- `availableTools`: array<string> (required)
- `capabilities`: object (required)
  - `instagramFeedPosts`: boolean (required)
  - `maxAccountsPerPost`: 2 (required)
  - `oneAccountPerPlatform`: true (required)
  - `maxCarouselImages`: 20 (required)
  - `scheduling`: earliest_release_worker_local (required)
  - `taskTypes`: array<string>
  - `taskReview`: humanpost
- `serverVersion`: string (required)

### list_accounts

Inspect account IDs, eligibility, daily usage, queue depth, and stored profilePictureUrl, bio, and warmupSearchTerms before choosing targets. Setup values may be requested; stored values do not independently verify the live platform profile or completed warm-up. One account per platform per post. Requires accounts:read.

**Input**
- `platform`: tiktok | instagram

**Output**
- `data`: array<object> (required)
  - `accountId`: string (required)
  - `platform`: tiktok | instagram (required)
  - `username`: string (required)
  - `displayName`: string | null (required)
  - `profilePictureUrl`: string | null (required) — Stored profile picture URL (profilePicture, falling back to avatarUrl); null when absent. Setup placeholders contain requested values. This is not independent verification of the live platform profile.
  - `bio`: string | null (required) — Stored account bio; null when absent and an empty string when explicitly stored empty. Setup placeholders contain requested values; pending profile edits retain the previous value until worker approval.
  - `warmupSearchTerms`: array<string> (required) — Stored account-specific warm-up search terms, normalized to strings; [] when absent. Historical lists may exceed the five-term creation limit. These terms do not verify completed warm-up activity.
  - `status`: string (required)
  - `timezone`: string (required)
  - `followers`: number (required)
  - `postsToday`: integer (required)
  - `dailyCap`: integer (required)
  - `queueDepth`: integer (required)
  - `nextWindow`: string | null (required) [format="date-time"]
  - `lastPostAt`: string | null (required) [format="date-time"]

### get_account

Read one company-owned publishing account, including stored profilePictureUrl, bio, and warmupSearchTerms. Setup values may be requested; pending profile edits retain previous values until worker approval. This does not independently verify the live platform profile or completed warm-up. Requires accounts:read.

**Input**
- `accountId`: string (required) [minLength=1, maxLength=200]

**Output**
- `accountId`: string (required)
- `platform`: tiktok | instagram (required)
- `username`: string (required)
- `displayName`: string | null (required)
- `profilePictureUrl`: string | null (required) — Stored profile picture URL (profilePicture, falling back to avatarUrl); null when absent. Setup placeholders contain requested values. This is not independent verification of the live platform profile.
- `bio`: string | null (required) — Stored account bio; null when absent and an empty string when explicitly stored empty. Setup placeholders contain requested values; pending profile edits retain the previous value until worker approval.
- `warmupSearchTerms`: array<string> (required) — Stored account-specific warm-up search terms, normalized to strings; [] when absent. Historical lists may exceed the five-term creation limit. These terms do not verify completed warm-up activity.
- `status`: string (required)
- `timezone`: string (required)
- `followers`: number (required)
- `postsToday`: integer (required)
- `dailyCap`: integer (required)
- `queueDepth`: integer (required)
- `nextWindow`: string | null (required) [format="date-time"]
- `lastPostAt`: string | null (required) [format="date-time"]

### get_company_schedule

Read the company's daily posting windows; account overrides take precedence. Requires accounts:read.

**Input**


**Output**
- `companyId`: string (required)
- `schedule`: object | null (required)
  - `windows`: array<string> (required) [minItems=1, maxItems=4]
- `source`: company | global (required)
- `scheduledPostsPerDay`: integer | null (required) — Selected window count, or null when global gap-based rules apply.
- `maxPostsPerDay`: 4 (required) — Hard daily limit per account, including make-up posts.
- `timezoneMode`: worker_local (required)

### update_company_schedule

Replace company windows while preserving account overrides. Example: {schedule:{windows:['08:00','17:00']}}. null restores global rules, not a pause; future dispatches only, in worker-local time, up to four posts/day including make-up posts. Requires accounts:write.

**Input**
- `schedule`: object | null (required) — Replace the active windows with 1–4 sorted HH:mm starts, at least 3 hours apart, between 08:00 and 17:00 inclusive. Standard slots: 08:00, 11:00, 14:00, 17:00. Times follow each assigned worker's timezone. null clears the override; it does not pause posting.
  - `windows`: array<string> (required) [minItems=1, maxItems=4]

**Output**
- `companyId`: string (required)
- `schedule`: object | null (required)
  - `windows`: array<string> (required) [minItems=1, maxItems=4]
- `source`: company | global (required)
- `scheduledPostsPerDay`: integer | null (required) — Selected window count, or null when global gap-based rules apply.
- `maxPostsPerDay`: 4 (required) — Hard daily limit per account, including make-up posts.
- `timezoneMode`: worker_local (required)

### get_account_schedule

Read the override, company default, effective windows, source, and worker timezone. Requires accounts:read.

**Input**
- `accountId`: string (required) [minLength=1, maxLength=200]

**Output**
- `accountId`: string (required)
- `schedule`: object | null (required) — Account override; null means inherit.
  - `windows`: array<string> (required) [minItems=1, maxItems=4]
- `companySchedule`: object | null (required)
  - `windows`: array<string> (required) [minItems=1, maxItems=4]
- `effectiveSchedule`: object | null (required)
  - `windows`: array<string> (required) [minItems=1, maxItems=4]
- `source`: account | company | global (required)
- `scheduledPostsPerDay`: integer | null (required)
- `maxPostsPerDay`: 4 (required)
- `timezoneMode`: worker_local (required)
- `timezone`: string (required)

### update_account_schedule

Replace an account's daily worker-local windows. schedule:null restores company inheritance, not a pause. Future dispatches only, up to four posts/day including make-up posts. Requires accounts:write.

**Input**
- `schedule`: object | null (required) — Replace the active windows with 1–4 sorted HH:mm starts, at least 3 hours apart, between 08:00 and 17:00 inclusive. Standard slots: 08:00, 11:00, 14:00, 17:00. Times follow each assigned worker's timezone. null clears the override; it does not pause posting.
  - `windows`: array<string> (required) [minItems=1, maxItems=4]
- `accountId`: string (required) [minLength=1, maxLength=200]

**Output**
- `accountId`: string (required)
- `schedule`: object | null (required) — Account override; null means inherit.
  - `windows`: array<string> (required) [minItems=1, maxItems=4]
- `companySchedule`: object | null (required)
  - `windows`: array<string> (required) [minItems=1, maxItems=4]
- `effectiveSchedule`: object | null (required)
  - `windows`: array<string> (required) [minItems=1, maxItems=4]
- `source`: account | company | global (required)
- `scheduledPostsPerDay`: integer | null (required)
- `maxPostsPerDay`: 4 (required)
- `timezoneMode`: worker_local (required)
- `timezone`: string (required)

### get_account_queue

Page through queued targets. Pass nextCursor back as cursor, even after an empty page. scheduledAfter is an earliest release instant, not a guaranteed posting time. Requires posts:read.

**Input**
- `limit`: integer [minimum=1, maximum=100, default=25]
- `cursor`: string [minLength=1]
- `accountId`: string (required) [minLength=1, maxLength=200]

**Output**
- `data`: array<object> (required)
  - `postId`: string (required)
  - `queueItemId`: string (required)
  - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
  - `caption`: string (required)
  - `thumbnailUrl`: string | null (required) [format="uri"]
  - `queuedAt`: string (required) [format="date-time"]
  - `scheduledAfter`: string | null (required) [format="date-time"]
  - `expiresAt`: string | null [format="date-time"]
  - `scheduledSlotStart`: string | null (required) [format="date-time"]
- `nextCursor`: string | null (required)

### create_account

Submit a complete TikTok or Instagram profile to the worker setup pool. Account setup is asynchronous. Requires accounts:write.

**Input**
- `platform`: tiktok | instagram (required)
- `username`: string (required)
- `displayName`: string (required) [minLength=1, maxLength=80]
- `bio`: string (required) [minLength=1, maxLength=220]
- `profilePictureUrl`: string (required) [maxLength=2048, format="uri"]
- `bioUrl`: string [maxLength=2048, format="uri"]
- `warmup`: object (required)
  - `searchTerms`: array<string> (required) [minItems=1, maxItems=5] — 1–5 account-specific warm-up search terms. Existing accounts may contain more than five.

**Output**
- `accountId`: string (required)
- `platform`: tiktok | instagram (required)
- `username`: string (required)
- `displayName`: string | null (required)
- `profilePictureUrl`: string | null (required) — Stored profile picture URL (profilePicture, falling back to avatarUrl); null when absent. Setup placeholders contain requested values. This is not independent verification of the live platform profile.
- `bio`: string | null (required) — Stored account bio; null when absent and an empty string when explicitly stored empty. Setup placeholders contain requested values; pending profile edits retain the previous value until worker approval.
- `warmupSearchTerms`: array<string> (required) — Stored account-specific warm-up search terms, normalized to strings; [] when absent. Historical lists may exceed the five-term creation limit. These terms do not verify completed warm-up activity.
- `status`: string (required)
- `timezone`: string (required)
- `followers`: number (required)
- `postsToday`: integer (required)
- `dailyCap`: integer (required)
- `queueDepth`: integer (required)
- `nextWindow`: string | null (required) [format="date-time"]
- `lastPostAt`: string | null (required) [format="date-time"]

### upload_media

Import a public sourceUrl (auto-completed), or provide filename, contentType and actual sizeBytes for a signed PUT. After PUT with requiredHeaders, call complete_upload. Videos: MP4/MOV, H.264/HEVC, AAC, <=200MB, <=3 minutes, <=2160x3840, <=60fps; images: JPG/PNG/WebP/HEIC <=50MB. Supported video is not transcoded. Requires posts:write.

**Input**
- Variant 1:
  - `kind`: video | image (required)
  - `filename`: string (required) [minLength=1, maxLength=255] — For video uploads, the filename must use the .mp4 or .mov extension.
  - `contentType`: string (required) [minLength=1, maxLength=100] — For video uploads, use video/mp4 for MP4 or video/quicktime for MOV.
  - `sizeBytes`: integer (required) [exclusiveMinimum=0] — Video uploads may be at most 209,715,200 bytes (200 MiB); images may be at most 52,428,800 bytes (50 MiB).
- Variant 2:
  - `kind`: video | image (required)
  - `sourceUrl`: string (required) [maxLength=2048, format="uri"]
  - `filename`: string [minLength=1, maxLength=255]

**Output**
- Variant 1:
  - `uploadId`: string (required)
  - `uploadUrl`: string (required) [format="uri"]
  - `expiresAt`: string (required) [format="date-time"]
  - `requiredHeaders`: object (required)
    - `Content-Type`: string (required)
- Variant 2:
  - `uploadId`: string (required)
  - `status`: ready (required)

### get_upload_status

Inspect pending, ready, or failed uploads and corrective errors. Requires posts:read.

**Input**
- `uploadId`: string (required) [minLength=1, maxLength=200]

**Output**
- `uploadId`: string (required)
- `kind`: video | image (required)
- `status`: pending | ready | failed (required)
- `url`: string [format="uri"]
- `thumbnailUrl`: string | null [format="uri"]
- `error`: string

### complete_upload

Validate media after the signed PUT finishes. Ready uploads replay safely. Requires posts:write.

**Input**
- `uploadId`: string (required) [minLength=1, maxLength=200]

**Output**
- `uploadId`: string (required)
- `status`: ready (required)

### create_upload_batch

Start a resumable, ordered import of up to 100 public media URLs. Poll get_job; results retain input indices and per-file errors. Use an idempotencyKey for safe retries. Requires posts:write.

**Input**
- `items`: array<object> (required) [minItems=1, maxItems=100]
  - `kind`: video | image (required)
  - `sourceUrl`: string (required) [maxLength=2048, format="uri"]
  - `filename`: string [minLength=1, maxLength=255]
- `idempotencyKey`: string [minLength=1, maxLength=200] — Keep this key and the payload unchanged when retrying the same operation.

**Output**
- `jobId`: string (required)
- `type`: upload_batch | preview | analytics_report (required)
- `status`: pending | running | complete | partially_failed | failed (required)
- `progress`: integer (required) [minimum=0]
- `total`: integer | null (required) [minimum=0]
- `createdAt`: string (required) [format="date-time"]
- `updatedAt`: string (required) [format="date-time"]
- `results`: array<object>
  - `index`: integer (required)
  - `uploadId`: string
  - `status`: ready | failed (required)
  - `error`: object
    - `code`: string (required)
    - `message`: string (required)
    - `hint`: string
    - `requestId`: string (required)
    - `retryable`: boolean (required)
    - `details`: array<object>
      - `field`: string (required)
      - `message`: string (required)
    - `retryAfterSeconds`: number [minimum=0]
- `report`: object
- `preview`: object
  - `postId`: string (required)
  - `versionId`: string | null (required)
  - `status`: ready | pending | failed | stale (required)
  - `mediaUrls`: array<string> (required)
  - `jobId`: string
  - `error`: object
    - `code`: string (required)
    - `message`: string (required)
    - `hint`: string
    - `requestId`: string (required)
    - `retryable`: boolean (required)
    - `details`: array<object>
      - `field`: string (required)
      - `message`: string (required)
    - `retryAfterSeconds`: number [minimum=0]
- `error`: object
  - `code`: string (required)
  - `message`: string (required)
  - `hint`: string
  - `requestId`: string (required)
  - `retryable`: boolean (required)
  - `details`: array<object>
    - `field`: string (required)
    - `message`: string (required)
  - `retryAfterSeconds`: number [minimum=0]

### get_job

Poll a batch upload, draft preview, or analytics report. Access requires the job's scope. Completed jobs return their ordered results or report.

**Input**
- `jobId`: string (required) [minLength=1, maxLength=200]

**Output**
- `jobId`: string (required)
- `type`: upload_batch | preview | analytics_report (required)
- `status`: pending | running | complete | partially_failed | failed (required)
- `progress`: integer (required) [minimum=0]
- `total`: integer | null (required) [minimum=0]
- `createdAt`: string (required) [format="date-time"]
- `updatedAt`: string (required) [format="date-time"]
- `results`: array<object>
  - `index`: integer (required)
  - `uploadId`: string
  - `status`: ready | failed (required)
  - `error`: object
    - `code`: string (required)
    - `message`: string (required)
    - `hint`: string
    - `requestId`: string (required)
    - `retryable`: boolean (required)
    - `details`: array<object>
      - `field`: string (required)
      - `message`: string (required)
    - `retryAfterSeconds`: number [minimum=0]
- `report`: object
- `preview`: object
  - `postId`: string (required)
  - `versionId`: string | null (required)
  - `status`: ready | pending | failed | stale (required)
  - `mediaUrls`: array<string> (required)
  - `jobId`: string
  - `error`: object
    - `code`: string (required)
    - `message`: string (required)
    - `hint`: string
    - `requestId`: string (required)
    - `retryable`: boolean (required)
    - `details`: array<object>
      - `field`: string (required)
      - `message`: string (required)
    - `retryAfterSeconds`: number [minimum=0]
- `error`: object
  - `code`: string (required)
  - `message`: string (required)
  - `hint`: string
  - `requestId`: string (required)
  - `retryable`: boolean (required)
  - `details`: array<object>
    - `field`: string (required)
    - `message`: string (required)
  - `retryAfterSeconds`: number [minimum=0]

### create_post

Create one-video or 2–20-image post and submit it to up to one account per platform. Managed approval rules apply unless skipApproval is explicitly requested. For review first, use create_draft. Use idempotencyKey for safe retries; scheduledAfter/expiresAt define a release interval, not exact publication times. Requires posts:write.

**Input**
- `media`: array<string> (required) [minItems=1, maxItems=20]
- `accountIds`: array<string> (required) [minItems=1, maxItems=2] — Choose at most one TikTok account and one Instagram account; duplicate platforms are rejected.
- `skipApproval`: boolean — When true, send the post directly to the posting queue instead of waiting for dashboard approval.
- `caption`: string [maxLength=2200]
- `hashtags`: array<string> [maxItems=30]
- `sound`: object
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string [maxLength=2000]
- `priority`: integer [minimum=0]
- `scheduledAfter`: string [format="date-time"] — Earliest release instant, expressed as ISO 8601 with a timezone offset. Actual publishing follows worker-local windows and capacity; this is not an exact posting time.
- `expiresAt`: string [format="date-time"] — Stop releasing queued targets at this instant. Does not recall work already released to an operator.
- `contentGroupIds`: array<string> [maxItems=10] — Optional tag or template reporting-group IDs. Use the contentGroupId returned by POST /v2/content-tags. Multiple groups are allowed; omit this field for no explicit tags.
- `instagramPostFormat`: reel | post — How Instagram should publish the media. Regular posts require a company capability; Reels are the default.
- `slides`: array<object> [minItems=2, maxItems=20]
  - `image`: string (required) [minLength=1, maxLength=200]
  - `textElements`: array<object> (required) [maxItems=6]
    - `content`: string (required) [minLength=1, maxLength=500]
    - `style`: object (required)
      - `backgroundColor`: none | black | white | red | yellow | blue [default="none"]
      - `textColor`: white | black | red | yellow | blue [default="white"]
      - `fontSize`: number [minimum=6, maximum=72, default=16]
      - `fontWeight`: normal | bold [default="normal"]
      - `textAlign`: left | center | right [default="center"]
      - `textMode`: stroke | background | plain | snapchat [default="stroke"]
      - `positionX`: number [minimum=0, maximum=100, default=50]
      - `positionY`: number (required) [minimum=0, maximum=100]
      - `width`: number [minimum=20, maximum=100, default=88]
      - `padding`: number [minimum=0, maximum=20, default=0]
  - `imageFit`: fit | fill [default="fill"]
  - `role`: hook | middle | cta
- `idempotencyKey`: string [minLength=1, maxLength=200] — Keep this key and the payload unchanged when retrying the same operation.

**Output**
- `postId`: string (required)
- `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
- `media`: object (required)
  - `kind`: video | image (required)
  - `count`: integer (required)
  - `thumbnailUrl`: string | null (required) [format="uri"]
- `caption`: string | null (required)
- `hashtags`: array<string> (required)
- `sound`: object | null (required)
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string | null (required)
- `contentGroupIds`: array<string> (required) — All reporting groups attributed to the post, including its implicit template provenance and any explicitly assigned tags.
- `instagramPostFormat`: reel | post (required)
- `createdAt`: string (required) [format="date-time"]
- `accounts`: array<object> (required)
  - `accountId`: string (required)
  - `username`: string (required)
  - `platform`: tiktok | instagram (required)
  - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
  - `queuedAt`: string (required) [format="date-time"]
  - `approvedAt`: string [format="date-time"]
  - `scheduledAfter`: string [format="date-time"] — Earliest release instant. Actual dispatch still follows the account schedule and worker availability.
  - `expiresAt`: string [format="date-time"]
  - `scheduledSlotStart`: string [format="date-time"]
  - `postedAt`: string [format="date-time"]
  - `postUrl`: string [format="uri"]

### create_draft

Prepare a video or carousel without queueing or publishing it. Optional slides keep text editable. Use idempotencyKey for safe retries, then inspect content and request a preview before queue_post. Requires posts:write.

**Input**
- `media`: array<string> (required) [minItems=1, maxItems=20]
- `caption`: string [maxLength=2200]
- `hashtags`: array<string> [maxItems=30]
- `sound`: object
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string [maxLength=2000]
- `contentGroupIds`: array<string> [maxItems=10] — Optional tag or template reporting-group IDs. Use the contentGroupId returned by POST /v2/content-tags. Multiple groups are allowed; omit this field for no explicit tags.
- `instagramPostFormat`: reel | post — How Instagram should publish the media. Regular posts require a company capability; Reels are the default.
- `slides`: array<object> [minItems=2, maxItems=20]
  - `image`: string (required) [minLength=1, maxLength=200]
  - `textElements`: array<object> (required) [maxItems=6]
    - `content`: string (required) [minLength=1, maxLength=500]
    - `style`: object (required)
      - `backgroundColor`: none | black | white | red | yellow | blue [default="none"]
      - `textColor`: white | black | red | yellow | blue [default="white"]
      - `fontSize`: number [minimum=6, maximum=72, default=16]
      - `fontWeight`: normal | bold [default="normal"]
      - `textAlign`: left | center | right [default="center"]
      - `textMode`: stroke | background | plain | snapchat [default="stroke"]
      - `positionX`: number [minimum=0, maximum=100, default=50]
      - `positionY`: number (required) [minimum=0, maximum=100]
      - `width`: number [minimum=20, maximum=100, default=88]
      - `padding`: number [minimum=0, maximum=20, default=0]
  - `imageFit`: fit | fill [default="fill"]
  - `role`: hook | middle | cta
- `idempotencyKey`: string [minLength=1, maxLength=200] — Keep this key and the payload unchanged when retrying the same operation.

**Output**
- `postId`: string (required)
- `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
- `media`: object (required)
  - `kind`: video | image (required)
  - `count`: integer (required)
  - `thumbnailUrl`: string | null (required) [format="uri"]
- `caption`: string | null (required)
- `hashtags`: array<string> (required)
- `sound`: object | null (required)
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string | null (required)
- `contentGroupIds`: array<string> (required) — All reporting groups attributed to the post, including its implicit template provenance and any explicitly assigned tags.
- `instagramPostFormat`: reel | post (required)
- `createdAt`: string (required) [format="date-time"]
- `accounts`: array<object> (required)
  - `accountId`: string (required)
  - `username`: string (required)
  - `platform`: tiktok | instagram (required)
  - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
  - `queuedAt`: string (required) [format="date-time"]
  - `approvedAt`: string [format="date-time"]
  - `scheduledAfter`: string [format="date-time"] — Earliest release instant. Actual dispatch still follows the account schedule and worker availability.
  - `expiresAt`: string [format="date-time"]
  - `scheduledSlotStart`: string [format="date-time"]
  - `postedAt`: string [format="date-time"]
  - `postUrl`: string [format="uri"]

### get_post_content

Read media URLs, slide text/styles, and versionId. Background image URLs expire in 15 minutes. Requires posts:read.

**Input**
- `postId`: string (required) [minLength=1, maxLength=200]

**Output**
- `post`: object (required)
  - `postId`: string (required)
  - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
  - `media`: object (required)
    - `kind`: video | image (required)
    - `count`: integer (required)
    - `thumbnailUrl`: string | null (required) [format="uri"]
  - `caption`: string | null (required)
  - `hashtags`: array<string> (required)
  - `sound`: object | null (required)
    - `mode`: trending | original | specific (required)
    - `url`: string [format="uri"]
    - `urlsByPlatform`: object
      - `tiktok`: string [format="uri"]
      - `instagram`: string [format="uri"]
  - `notes`: string | null (required)
  - `contentGroupIds`: array<string> (required) — All reporting groups attributed to the post, including its implicit template provenance and any explicitly assigned tags.
  - `instagramPostFormat`: reel | post (required)
  - `createdAt`: string (required) [format="date-time"]
  - `accounts`: array<object> (required)
    - `accountId`: string (required)
    - `username`: string (required)
    - `platform`: tiktok | instagram (required)
    - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
    - `queuedAt`: string (required) [format="date-time"]
    - `approvedAt`: string [format="date-time"]
    - `scheduledAfter`: string [format="date-time"] — Earliest release instant. Actual dispatch still follows the account schedule and worker availability.
    - `expiresAt`: string [format="date-time"]
    - `scheduledSlotStart`: string [format="date-time"]
    - `postedAt`: string [format="date-time"]
    - `postUrl`: string [format="uri"]
- `versionId`: string | null (required)
- `media`: array<object> (required)
  - `uploadId`: string | null (required)
  - `url`: string | null (required)
- `slides`: array<object> (required)
  - `slideId`: string (required)
  - `image`: string | null (required)
  - `imageUrl`: string | null (required)
  - `textElements`: array<object> (required)
    - `id`: string
    - `content`: string (required)
    - `style`: object (required)
  - `imageFit`: fit | fill (required)
  - `role`: string

### update_draft

Replace draft media or slides, or edit metadata. expectedVersionId detects conflicting artwork edits. Submitted or published posts are rejected. Preview the new version before submitting it. Requires posts:write.

**Input**
- `media`: array<string> [minItems=1, maxItems=20]
- `caption`: string [maxLength=2200]
- `hashtags`: array<string> [maxItems=30]
- `sound`: object
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string [maxLength=2000]
- `contentGroupIds`: array<string> [maxItems=10] — Optional tag or template reporting-group IDs. Use the contentGroupId returned by POST /v2/content-tags. Multiple groups are allowed; omit this field for no explicit tags.
- `instagramPostFormat`: reel | post — How Instagram should publish the media. Regular posts require a company capability; Reels are the default.
- `slides`: array<object> [minItems=2, maxItems=20]
  - `image`: string (required) [minLength=1, maxLength=200]
  - `textElements`: array<object> (required) [maxItems=6]
    - `content`: string (required) [minLength=1, maxLength=500]
    - `style`: object (required)
      - `backgroundColor`: none | black | white | red | yellow | blue [default="none"]
      - `textColor`: white | black | red | yellow | blue [default="white"]
      - `fontSize`: number [minimum=6, maximum=72, default=16]
      - `fontWeight`: normal | bold [default="normal"]
      - `textAlign`: left | center | right [default="center"]
      - `textMode`: stroke | background | plain | snapchat [default="stroke"]
      - `positionX`: number [minimum=0, maximum=100, default=50]
      - `positionY`: number (required) [minimum=0, maximum=100]
      - `width`: number [minimum=20, maximum=100, default=88]
      - `padding`: number [minimum=0, maximum=20, default=0]
  - `imageFit`: fit | fill [default="fill"]
  - `role`: hook | middle | cta
- `expectedVersionId`: string [minLength=1, maxLength=200] — Reject a stale artwork edit rather than overwriting another editor's version.
- `postId`: string (required) [minLength=1, maxLength=200]

**Output**
- `post`: object (required)
  - `postId`: string (required)
  - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
  - `media`: object (required)
    - `kind`: video | image (required)
    - `count`: integer (required)
    - `thumbnailUrl`: string | null (required) [format="uri"]
  - `caption`: string | null (required)
  - `hashtags`: array<string> (required)
  - `sound`: object | null (required)
    - `mode`: trending | original | specific (required)
    - `url`: string [format="uri"]
    - `urlsByPlatform`: object
      - `tiktok`: string [format="uri"]
      - `instagram`: string [format="uri"]
  - `notes`: string | null (required)
  - `contentGroupIds`: array<string> (required) — All reporting groups attributed to the post, including its implicit template provenance and any explicitly assigned tags.
  - `instagramPostFormat`: reel | post (required)
  - `createdAt`: string (required) [format="date-time"]
  - `accounts`: array<object> (required)
    - `accountId`: string (required)
    - `username`: string (required)
    - `platform`: tiktok | instagram (required)
    - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
    - `queuedAt`: string (required) [format="date-time"]
    - `approvedAt`: string [format="date-time"]
    - `scheduledAfter`: string [format="date-time"] — Earliest release instant. Actual dispatch still follows the account schedule and worker availability.
    - `expiresAt`: string [format="date-time"]
    - `scheduledSlotStart`: string [format="date-time"]
    - `postedAt`: string [format="date-time"]
    - `postUrl`: string [format="uri"]
- `versionId`: string | null (required)
- `media`: array<object> (required)
  - `uploadId`: string | null (required)
  - `url`: string | null (required)
- `slides`: array<object> (required)
  - `slideId`: string (required)
  - `image`: string | null (required)
  - `imageUrl`: string | null (required)
  - `textElements`: array<object> (required)
    - `id`: string
    - `content`: string (required)
    - `style`: object (required)
  - `imageFit`: fit | fill (required)
  - `role`: string

### request_post_preview

Start a durable render of the current unqueued draft version without approving or queueing it. Poll get_job or get_post_preview. Old renders cannot replace newer artwork. Requires posts:write.

**Input**
- `postId`: string (required) [minLength=1, maxLength=200]
- `idempotencyKey`: string [minLength=1, maxLength=200] — Keep this key and the payload unchanged when retrying the same operation.

**Output**
- `jobId`: string (required)
- `type`: upload_batch | preview | analytics_report (required)
- `status`: pending | running | complete | partially_failed | failed (required)
- `progress`: integer (required) [minimum=0]
- `total`: integer | null (required) [minimum=0]
- `createdAt`: string (required) [format="date-time"]
- `updatedAt`: string (required) [format="date-time"]
- `results`: array<object>
  - `index`: integer (required)
  - `uploadId`: string
  - `status`: ready | failed (required)
  - `error`: object
    - `code`: string (required)
    - `message`: string (required)
    - `hint`: string
    - `requestId`: string (required)
    - `retryable`: boolean (required)
    - `details`: array<object>
      - `field`: string (required)
      - `message`: string (required)
    - `retryAfterSeconds`: number [minimum=0]
- `report`: object
- `preview`: object
  - `postId`: string (required)
  - `versionId`: string | null (required)
  - `status`: ready | pending | failed | stale (required)
  - `mediaUrls`: array<string> (required)
  - `jobId`: string
  - `error`: object
    - `code`: string (required)
    - `message`: string (required)
    - `hint`: string
    - `requestId`: string (required)
    - `retryable`: boolean (required)
    - `details`: array<object>
      - `field`: string (required)
      - `message`: string (required)
    - `retryAfterSeconds`: number [minimum=0]
- `error`: object
  - `code`: string (required)
  - `message`: string (required)
  - `hint`: string
  - `requestId`: string (required)
  - `retryable`: boolean (required)
  - `details`: array<object>
    - `field`: string (required)
    - `message`: string (required)
  - `retryAfterSeconds`: number [minimum=0]

### get_post_preview

Read the current version's preview URLs and pending/ready/failed status. A preview does not submit a draft. Requires posts:read.

**Input**
- `postId`: string (required) [minLength=1, maxLength=200]

**Output**
- `postId`: string (required)
- `versionId`: string | null (required)
- `status`: ready | pending | failed | stale (required)
- `mediaUrls`: array<string> (required)
- `jobId`: string
- `error`: object
  - `code`: string (required)
  - `message`: string (required)
  - `hint`: string
  - `requestId`: string (required)
  - `retryable`: boolean (required)
  - `details`: array<object>
    - `field`: string (required)
    - `message`: string (required)
  - `retryAfterSeconds`: number [minimum=0]

### queue_post

Queue an existing draft with ready media to at most one account per platform. No media creation. Managed approval rules apply unless skipApproval is explicitly requested. Use idempotencyKey for safe retries; scheduledAfter/expiresAt define a release interval. Requires posts:write.

**Input**
- `accountIds`: array<string> (required) [minItems=1, maxItems=2] — Choose at most one TikTok account and one Instagram account; duplicate platforms are rejected.
- `skipApproval`: boolean — When true, send the post directly to the posting queue instead of waiting for dashboard approval.
- `caption`: string [maxLength=2200]
- `hashtags`: array<string> [maxItems=30]
- `sound`: object
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string [maxLength=2000]
- `priority`: integer [minimum=0]
- `scheduledAfter`: string [format="date-time"] — Earliest release instant, expressed as ISO 8601 with a timezone offset. Actual publishing follows worker-local windows and capacity; this is not an exact posting time.
- `expiresAt`: string [format="date-time"] — Stop releasing queued targets at this instant. Does not recall work already released to an operator.
- `instagramPostFormat`: reel | post — How Instagram should publish the media. Regular posts require a company capability; Reels are the default.
- `postId`: string (required) [minLength=1, maxLength=200]
- `idempotencyKey`: string [minLength=1, maxLength=200] — Keep this key and the payload unchanged when retrying the same operation.

**Output**
- `postId`: string (required)
- `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
- `media`: object (required)
  - `kind`: video | image (required)
  - `count`: integer (required)
  - `thumbnailUrl`: string | null (required) [format="uri"]
- `caption`: string | null (required)
- `hashtags`: array<string> (required)
- `sound`: object | null (required)
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string | null (required)
- `contentGroupIds`: array<string> (required) — All reporting groups attributed to the post, including its implicit template provenance and any explicitly assigned tags.
- `instagramPostFormat`: reel | post (required)
- `createdAt`: string (required) [format="date-time"]
- `accounts`: array<object> (required)
  - `accountId`: string (required)
  - `username`: string (required)
  - `platform`: tiktok | instagram (required)
  - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
  - `queuedAt`: string (required) [format="date-time"]
  - `approvedAt`: string [format="date-time"]
  - `scheduledAfter`: string [format="date-time"] — Earliest release instant. Actual dispatch still follows the account schedule and worker availability.
  - `expiresAt`: string [format="date-time"]
  - `scheduledSlotStart`: string [format="date-time"]
  - `postedAt`: string [format="date-time"]
  - `postUrl`: string [format="uri"]

### get_post_status

Read aggregate and per-account publishing status, release times, and posted URLs. Requires posts:read.

**Input**
- `postId`: string (required) [minLength=1, maxLength=200]

**Output**
- `postId`: string (required)
- `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
- `media`: object (required)
  - `kind`: video | image (required)
  - `count`: integer (required)
  - `thumbnailUrl`: string | null (required) [format="uri"]
- `caption`: string | null (required)
- `hashtags`: array<string> (required)
- `sound`: object | null (required)
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string | null (required)
- `contentGroupIds`: array<string> (required) — All reporting groups attributed to the post, including its implicit template provenance and any explicitly assigned tags.
- `instagramPostFormat`: reel | post (required)
- `createdAt`: string (required) [format="date-time"]
- `accounts`: array<object> (required)
  - `accountId`: string (required)
  - `username`: string (required)
  - `platform`: tiktok | instagram (required)
  - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
  - `queuedAt`: string (required) [format="date-time"]
  - `approvedAt`: string [format="date-time"]
  - `scheduledAfter`: string [format="date-time"] — Earliest release instant. Actual dispatch still follows the account schedule and worker availability.
  - `expiresAt`: string [format="date-time"]
  - `scheduledSlotStart`: string [format="date-time"]
  - `postedAt`: string [format="date-time"]
  - `postUrl`: string [format="uri"]

### list_posts

Page through company posts filtered by account, status, creation date, or content group. Pass nextCursor as cursor, even if a filtered page has no results. Requires posts:read.

**Input**
- `limit`: integer [minimum=1, maximum=100, default=25]
- `cursor`: string [minLength=1]
- `accountId`: string [minLength=1, maxLength=200]
- `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed
- `since`: string [format="date-time"]
- `contentGroupId`: string or untagged — Return only posts attributed to this tag or template reporting group. Use `untagged` for posts with no tag or template attribution.
  - Variant 1:
  - Variant 2:

**Output**
- `data`: array<object> (required)
  - `postId`: string (required)
  - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
  - `media`: object (required)
    - `kind`: video | image (required)
    - `count`: integer (required)
    - `thumbnailUrl`: string | null (required) [format="uri"]
  - `caption`: string | null (required)
  - `hashtags`: array<string> (required)
  - `sound`: object | null (required)
    - `mode`: trending | original | specific (required)
    - `url`: string [format="uri"]
    - `urlsByPlatform`: object
      - `tiktok`: string [format="uri"]
      - `instagram`: string [format="uri"]
  - `notes`: string | null (required)
  - `contentGroupIds`: array<string> (required) — All reporting groups attributed to the post, including its implicit template provenance and any explicitly assigned tags.
  - `instagramPostFormat`: reel | post (required)
  - `createdAt`: string (required) [format="date-time"]
  - `accounts`: array<object> (required)
    - `accountId`: string (required)
    - `username`: string (required)
    - `platform`: tiktok | instagram (required)
    - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
    - `queuedAt`: string (required) [format="date-time"]
    - `approvedAt`: string [format="date-time"]
    - `scheduledAfter`: string [format="date-time"] — Earliest release instant. Actual dispatch still follows the account schedule and worker availability.
    - `expiresAt`: string [format="date-time"]
    - `scheduledSlotStart`: string [format="date-time"]
    - `postedAt`: string [format="date-time"]
    - `postUrl`: string [format="uri"]
- `nextCursor`: string | null (required)

### update_post

Edit metadata while every target remains editable. contentGroupIds replaces explicit tags and remains editable after publication; [] clears explicit tags while preserving template provenance. Requires posts:write.

**Input**
- `caption`: string [maxLength=2200]
- `hashtags`: array<string> [maxItems=30]
- `sound`: object
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string | null [maxLength=2000]
- `contentGroupIds`: array<string> [maxItems=10] — Replace the post's complete list of explicitly assigned tag or template reporting groups. This remains editable after publishing. To remove one tag, resend the list without it; pass [] to remove every explicit tag. Template provenance inherited from the post's original template remains visible.
- `instagramPostFormat`: reel | post — How Instagram should publish the media. `post` is rejected unless the company has regular feed posts enabled.
- `postId`: string (required) [minLength=1, maxLength=200]

**Output**
- `postId`: string (required)
- `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
- `media`: object (required)
  - `kind`: video | image (required)
  - `count`: integer (required)
  - `thumbnailUrl`: string | null (required) [format="uri"]
- `caption`: string | null (required)
- `hashtags`: array<string> (required)
- `sound`: object | null (required)
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string | null (required)
- `contentGroupIds`: array<string> (required) — All reporting groups attributed to the post, including its implicit template provenance and any explicitly assigned tags.
- `instagramPostFormat`: reel | post (required)
- `createdAt`: string (required) [format="date-time"]
- `accounts`: array<object> (required)
  - `accountId`: string (required)
  - `username`: string (required)
  - `platform`: tiktok | instagram (required)
  - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
  - `queuedAt`: string (required) [format="date-time"]
  - `approvedAt`: string [format="date-time"]
  - `scheduledAfter`: string [format="date-time"] — Earliest release instant. Actual dispatch still follows the account schedule and worker availability.
  - `expiresAt`: string [format="date-time"]
  - `scheduledSlotStart`: string [format="date-time"]
  - `postedAt`: string [format="date-time"]
  - `postUrl`: string [format="uri"]

### list_content_tags

Read active and archived company tags and stable contentGroupId values. Requires posts:read.

**Input**


**Output**
- `data`: array<object> (required)
  - `tagId`: string (required)
  - `contentGroupId`: string (required) [maxLength=240] — Stable value to send in a post's contentGroupIds array, for example `tag:abc123`.
  - `name`: string (required)
  - `color`: slate | blue | cyan | green | amber | orange | rose | violet (required)
  - `archived`: boolean (required)
  - `createdAt`: string (required) [format="date-time"]
  - `updatedAt`: string (required) [format="date-time"]

### create_content_tag

Create an optional internal reporting tag; it does not change public captions. Requires posts:write.

**Input**
- `name`: string (required) [minLength=1, maxLength=50] — Unique tag name within the API key's company.
- `color`: slate | blue | cyan | green | amber | orange | rose | violet [default="slate"] — Dashboard display color. Defaults to slate.

**Output**
- `tagId`: string (required)
- `contentGroupId`: string (required) [maxLength=240] — Stable value to send in a post's contentGroupIds array, for example `tag:abc123`.
- `name`: string (required)
- `color`: slate | blue | cyan | green | amber | orange | rose | violet (required)
- `archived`: boolean (required)
- `createdAt`: string (required) [format="date-time"]
- `updatedAt`: string (required) [format="date-time"]

### update_content_tag

Rename, recolor, archive, or restore a tag. Archiving preserves attribution. Requires posts:write.

**Input**
- `name`: string [minLength=1, maxLength=50] — New unique tag name.
- `color`: slate | blue | cyan | green | amber | orange | rose | violet — New dashboard display color.
- `archived`: boolean — Set true to hide the tag from new assignments without changing historical attribution; set false to restore it.
- `tagId`: string (required) [minLength=1, maxLength=200]

**Output**
- `tagId`: string (required)
- `contentGroupId`: string (required) [maxLength=240] — Stable value to send in a post's contentGroupIds array, for example `tag:abc123`.
- `name`: string (required)
- `color`: slate | blue | cyan | green | amber | orange | rose | violet (required)
- `archived`: boolean (required)
- `createdAt`: string (required) [format="date-time"]
- `updatedAt`: string (required) [format="date-time"]

### delete_content_tag

Remove a custom tag from all company posts. Archive instead to preserve history. Requires posts:write.

**Input**
- `tagId`: string (required) [minLength=1, maxLength=200]

**Output**
- `tagId`: string (required)
- `updatedPosts`: integer (required)

### cancel_post

Cancel pre-release targets and report targets already posting, posted, or cancelled as skipped. Published social posts are not deleted. Requires posts:write.

**Input**
- `postId`: string (required) [minLength=1, maxLength=200]
- `accountId`: string [minLength=1, maxLength=200]

**Output**
- `postId`: string (required)
- `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
- `media`: object (required)
  - `kind`: video | image (required)
  - `count`: integer (required)
  - `thumbnailUrl`: string | null (required) [format="uri"]
- `caption`: string | null (required)
- `hashtags`: array<string> (required)
- `sound`: object | null (required)
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string | null (required)
- `contentGroupIds`: array<string> (required) — All reporting groups attributed to the post, including its implicit template provenance and any explicitly assigned tags.
- `instagramPostFormat`: reel | post (required)
- `createdAt`: string (required) [format="date-time"]
- `accounts`: array<object> (required)
  - `accountId`: string (required)
  - `username`: string (required)
  - `platform`: tiktok | instagram (required)
  - `status`: draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed (required)
  - `queuedAt`: string (required) [format="date-time"]
  - `approvedAt`: string [format="date-time"]
  - `scheduledAfter`: string [format="date-time"] — Earliest release instant. Actual dispatch still follows the account schedule and worker availability.
  - `expiresAt`: string [format="date-time"]
  - `scheduledSlotStart`: string [format="date-time"]
  - `postedAt`: string [format="date-time"]
  - `postUrl`: string [format="uri"]
- `cancellation`: object (required)
  - `cancelled`: array<string> (required)
  - `skipped`: array<object> (required)
    - `accountId`: string (required)
    - `reason`: currently_posting | already_posted | already_cancelled (required)

### get_post_analytics

Read the latest summary and paginate historical destination observations. Inspect lastUpdated/collectedAt for freshness. Requires analytics:read.

**Input**
- `limit`: integer [minimum=1, maximum=100, default=25]
- `cursor`: string [minLength=1]
- `postId`: string (required) [minLength=1, maxLength=200]

**Output**
- `summary`: object | null (required)
  - `views`: integer [minimum=0, default=0]
  - `likes`: integer [minimum=0, default=0]
  - `comments`: integer [minimum=0, default=0]
  - `shares`: integer [minimum=0, default=0]
  - `saves`: integer [minimum=0, default=0]
  - `engagementRate`: number [minimum=0, default=0]
  - `lastUpdated`: string [format="date-time"]
  - `postUrl`: string [format="uri"]
  - `accountId`: string
  - `platform`: tiktok | instagram
- `history`: array<object> (required)
  - `id`: string (required)
  - `videoId`: string (required)
  - `accountId`: string (required)
  - `platform`: tiktok | instagram (required)
  - `postUrl`: string (required)
  - `views`: integer (required) [minimum=0]
  - `likes`: integer (required) [minimum=0]
  - `comments`: integer (required) [minimum=0]
  - `shares`: integer | null (required) [minimum=0]
  - `saves`: integer | null (required) [minimum=0]
  - `collectedAt`: string (required) [format="date-time"]
  - `taskId`: string | null
- `nextCursor`: string | null (required)

### get_account_analytics

Read legacy latest-200-post totals with scannedPosts and coverage metadata; use create_analytics_report for complete rollup comparisons. Requires analytics:read.

**Input**
- `accountId`: string (required) [minLength=1, maxLength=200]

**Output**
- `accountId`: string (required)
- `followers`: number (required)
- `posts`: number (required)
- `engagement`: number (required)
- `analytics`: object (required)
  - `views`: number (required)
  - `likes`: number (required)
  - `comments`: number (required)
- `scannedPosts`: integer (required)
- `coverage`: object (required)
  - `limit`: 200 (required)
  - `possiblyTruncated`: boolean (required)
  - `source`: string (required)
  - `destinationSpecific`: false (required)
  - `latestCollectedAt`: string | null (required)

### create_analytics_report

Create a durable company-scoped report of UTC-day growth, previous-period comparison, account/platform/tag/template breakdowns, and top destinations by latest cumulative views. Poll get_job. Requires active V3 rollups; availability and freshness are explicit. Requires analytics:read.

**Input**
- `from`: string (required) [format="date-time"]
- `to`: string (required) [format="date-time"]
- `comparePrevious`: boolean [default=true]
- `accountId`: string [minLength=1, maxLength=200]
- `platform`: tiktok | instagram
- `contentGroupId`: string [maxLength=240]
- `groupBy`: account | platform | content_group | template [default="account"]
- `topLimit`: integer [minimum=1, maximum=100, default=10]
- `idempotencyKey`: string [minLength=1, maxLength=200] — Keep this key and the payload unchanged when retrying the same operation.

**Output**
- `jobId`: string (required)
- `type`: upload_batch | preview | analytics_report (required)
- `status`: pending | running | complete | partially_failed | failed (required)
- `progress`: integer (required) [minimum=0]
- `total`: integer | null (required) [minimum=0]
- `createdAt`: string (required) [format="date-time"]
- `updatedAt`: string (required) [format="date-time"]
- `results`: array<object>
  - `index`: integer (required)
  - `uploadId`: string
  - `status`: ready | failed (required)
  - `error`: object
    - `code`: string (required)
    - `message`: string (required)
    - `hint`: string
    - `requestId`: string (required)
    - `retryable`: boolean (required)
    - `details`: array<object>
      - `field`: string (required)
      - `message`: string (required)
    - `retryAfterSeconds`: number [minimum=0]
- `report`: object
- `preview`: object
  - `postId`: string (required)
  - `versionId`: string | null (required)
  - `status`: ready | pending | failed | stale (required)
  - `mediaUrls`: array<string> (required)
  - `jobId`: string
  - `error`: object
    - `code`: string (required)
    - `message`: string (required)
    - `hint`: string
    - `requestId`: string (required)
    - `retryable`: boolean (required)
    - `details`: array<object>
      - `field`: string (required)
      - `message`: string (required)
    - `retryAfterSeconds`: number [minimum=0]
- `error`: object
  - `code`: string (required)
  - `message`: string (required)
  - `hint`: string
  - `requestId`: string (required)
  - `retryable`: boolean (required)
  - `details`: array<object>
    - `field`: string (required)
    - `message`: string (required)
  - `retryAfterSeconds`: number [minimum=0]

### get_usage

Read effective daily caps, counters, plan credits, and the next UTC reset. Requires usage:read.

**Input**


**Output**
- `taskCredits`: object
  - `comment`: object (required)
    - `monthlyLimit`: number (required)
    - `monthlyUsed`: number (required)
    - `monthlyAvailable`: number (required)
    - `oneTimeAvailable`: number (required)
    - `totalAvailable`: number (required)
    - `monthlyCreditsAvailable`: boolean (required)
  - `amplify`: object (required)
    - `monthlyLimit`: number (required)
    - `monthlyUsed`: number (required)
    - `monthlyAvailable`: number (required)
    - `oneTimeAvailable`: number (required)
    - `totalAvailable`: number (required)
    - `monthlyCreditsAvailable`: boolean (required)
  - `periodStart`: string | null (required) [format="date-time"]
  - `periodEnd`: string | null (required) [format="date-time"]
- `plan`: string | null (required)
- `billingType`: string | null (required)
- `videoQuota`: number | null (required)
- `videoCount`: number | null (required)
- `postCredits`: object | null (required)
  - `limit`: number (required)
  - `used`: number (required)
- `postsPerDay`: number (required)
- `apiRequestsToday`: number (required)
- `postsCreatedToday`: number (required)
- `uploadsCreatedToday`: number (required)
- `accountsCreatedToday`: number (required)
- `dailyLimits`: object (required)
  - `apiRequestsToday`: number (required)
  - `postsCreatedToday`: number (required)
  - `uploadsCreatedToday`: number (required)
  - `accountsCreatedToday`: number (required)
- `resetsAt`: string (required) [format="date-time"]

### get_workspace

Read company/account context. includeAssets:true adds a legacy bounded folder/image-pack snapshot; use paginated folder/image tools for efficient complete asset discovery. Requires workspace:read.

**Input**
- `includeAssets`: boolean [default=false]

**Output**
- `version`: 1 (required)
- `company`: object (required)
  - `companyId`: string (required)
  - `name`: string (required)
  - `description`: string
  - `website`: string
- `accounts`: array<object> (required)
  - `accountId`: string (required)
  - `platform`: tiktok | instagram (required)
  - `username`: string (required)
  - `displayName`: string | null (required)
  - `profilePictureUrl`: string | null (required) — Stored profile picture URL (profilePicture, falling back to avatarUrl); null when absent. Setup placeholders contain requested values. This is not independent verification of the live platform profile.
  - `bio`: string | null (required) — Stored account bio; null when absent and an empty string when explicitly stored empty. Setup placeholders contain requested values; pending profile edits retain the previous value until worker approval.
  - `warmupSearchTerms`: array<string> (required) — Stored account-specific warm-up search terms, normalized to strings; [] when absent. Historical lists may exceed the five-term creation limit. These terms do not verify completed warm-up activity.
  - `status`: string (required)
  - `timezone`: string (required)
  - `followers`: number (required)
  - `postsToday`: integer (required)
  - `dailyCap`: integer (required)
  - `queueDepth`: integer (required)
  - `nextWindow`: string | null (required) [format="date-time"]
  - `lastPostAt`: string | null (required) [format="date-time"]
  - `niche`: string [maxLength=160]
- `folders`: array<object> (required)
  - `folderId`: string (required)
  - `name`: string (required)
  - `createdAt`: string (required) [format="date-time"]
  - `images`: array<object> (required)
    - `imageId`: string (required)
    - `folderId`: string (required)
    - `label`: string (required)
    - `description`: string | null (required)
    - `tags`: array<string> (required)
    - `width`: number | null (required)
    - `height`: number | null (required)
    - `createdAt`: string (required) [format="date-time"]
  - `packs`: array<object> (required)
    - `packId`: string (required)
    - `folderId`: string (required)
    - `name`: string (required)
    - `description`: string | null (required)
    - `color`: string | null (required)
    - `imageIds`: array<string> (required)
    - `imageFit`: fit | fill (required)
    - `useImagesOnce`: boolean (required)
- `refreshedAt`: string (required) [format="date-time"]

### list_workspace_folders

Page through company folders with optional name search. Follow nextCursor even after an empty search page. Requires workspace:read.

**Input**
- `limit`: integer [minimum=1, maximum=100, default=25]
- `cursor`: string [minLength=1]
- `search`: string [minLength=1, maxLength=100]

**Output**
- `data`: array<object> (required)
  - `folderId`: string (required)
  - `name`: string (required)
  - `createdAt`: string (required) [format="date-time"]
- `nextCursor`: string | null (required)

### list_workspace_images

Page through available images in a company-owned folder, optionally searching labels, descriptions, or tags. Requires workspace:read.

**Input**
- `limit`: integer [minimum=1, maximum=100, default=25]
- `cursor`: string [minLength=1]
- `search`: string [minLength=1, maxLength=100]
- `folderId`: string (required) [minLength=1, maxLength=200]

**Output**
- `data`: array<object> (required)
  - `imageId`: string (required)
  - `folderId`: string (required)
  - `label`: string (required)
  - `description`: string | null (required)
  - `tags`: array<string> (required)
  - `width`: number | null (required)
  - `height`: number | null (required)
  - `createdAt`: string (required) [format="date-time"]
- `nextCursor`: string | null (required)

### get_workspace_media_urls

Resolve company-owned images to signed download URLs valid for 60–3600 seconds. Caller-supplied storage paths are never accepted. Requires media:read.

**Input**
- `items`: array<object> (required) [minItems=1, maxItems=100]
  - `folderId`: string (required) [minLength=1, maxLength=200]
  - `imageId`: string (required) [minLength=1, maxLength=200]
- `expiresInSeconds`: integer [minimum=60, maximum=3600, default=900]

**Output**
- `expiresAt`: string (required) [format="date-time"]
- `data`: array<object> (required)
  - `folderId`: string (required)
  - `imageId`: string (required)
  - `url`: string (required) [format="uri"]

## Endpoint reference

### GET /v2/task-types

Discover task capabilities

Requires tasks:read. Optionally pass accountId to inspect eligibility. Old blank-task-capable apps support new Amplify bundles. Worker business hours and HumanPost review apply.

**Parameters**
- `accountId` (query, optional): string

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 429 Error

### POST /v2/tasks

Create a comment or Amplify task

Requires tasks:write and Idempotency-Key. Reserves one Comment or Amplify credit atomically. Amplify is one public task with separately reviewed Repost + Like and Comment components. Keys are company-scoped; changed payloads conflict. Worker business hours apply. No admin, worker, payout or approval overrides.

**Parameters**
- `Idempotency-Key` (header, required): string

**JSON body**
- Variant 1:
  - `accountId`: string (required) [minLength=1, maxLength=200]
  - `externalReference`: string [minLength=1, maxLength=200]
  - `type`: comment_video (required)
  - `payload`: object (required)
    - `targetVideoUrl`: string (required) [maxLength=2000, format="uri"]
    - `commentText`: string (required) [minLength=1, maxLength=500]
    - `commentImageUrl`: string [maxLength=2000, format="uri"]
    - `allowAccountRollover`: boolean [default=false]
- Variant 2:
  - `accountId`: string (required) [minLength=1, maxLength=200]
  - `externalReference`: string [minLength=1, maxLength=200]
  - `type`: amplify_video (required)
  - `payload`: object (required)
    - `targetVideoUrl`: string (required) [maxLength=2000, format="uri"]
    - `commentText`: string (required) [minLength=1, maxLength=500]

**Responses:** 200 Idempotent replay; 201 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 429 Error

### GET /v2/tasks

List company tasks

Requires tasks:read. Bounds each scanned page; follow nextCursor even after an empty filtered page. Hides operational children/replacements and reports their effective outcome under the original task ID. Legacy tasks missing companyId are accessible by ID after account ownership verification, but absent from this company-indexed list.

**Parameters**
- `limit` (query, optional): integer
- `cursor` (query, optional): string
- `type` (query, optional): post_video | amplify_video | repost_like_video | like_video | comment_video | reply_to_comment | like_comment | follow_account | edit_bio | edit_username | edit_display_name | update_profile_pic | account_login | account_logout | account_setup | warmup_scroll | record_fyp | record_reels | amazon_review | g2_review | trustpilot_review | intro_video | spark_code | connect_posting_service | delete_post | delete_account | blank_task
- `status` (query, optional): pending | assigned | in_progress | pending_approval | completed | failed | expired | cancelled | unknown
- `accountId` (query, optional): string
- `platform` (query, optional): tiktok | instagram
- `externalReference` (query, optional): string
- `createdAfter` (query, optional): string
- `createdBefore` (query, optional): string

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 429 Error

### GET /v2/tasks/{taskId}

Inspect task progress

Requires tasks:read. Read combined Amplify components or comment replacement attempts. Completed means approved by HumanPost, not merely submitted. Use availableActions to discover supported controls. Operational child IDs cannot be queried independently through this company interface.

**Parameters**
- `taskId` (path, required): string

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 429 Error

### GET /v2/tasks/{taskId}/events

Read task history

Requires tasks:read. Persisted revisions arrive asynchronously and may be delivered out of order; eventId deduplicates, revision identifies the document version. historyComplete is false for legacy work. Poll without a cursor to see newer events; cursors page backward through history.

**Parameters**
- `taskId` (path, required): string
- `limit` (query, optional): integer
- `cursor` (query, optional): string

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 429 Error

### GET /v2/tasks/{taskId}/evidence

Read task screenshots

Requires tasks:evidence:read. Pages current evidence then archived Amplify proof. Follow nextCursor even after an empty page. Company-authorized screenshots use signed URLs valid for 60–3600 seconds. URLs are bearer links until expiry. Never exposes permanent Firebase download tokens or worker personal records.

**Parameters**
- `taskId` (path, required): string
- `limit` (query, optional): integer
- `cursor` (query, optional): string
- `expiresInSeconds` (query, optional): integer

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 429 Error

### POST /v2/tasks/{taskId}/cancel

Cancel remaining task work

Requires tasks:write. Supports unfinished Comment and Amplify requests. Retains proof/history; cannot undo platform actions. Amplify refunds only when neither component was approved. Idempotent repeats do not refund twice. Old-period refunds do not replenish a new period.

**Parameters**
- `taskId` (path, required): string

**JSON body**
- `reason`: string (required) [minLength=1, maxLength=500]

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 429 Error

### POST /v2/tasks/{taskId}/resume

Resume blocked Amplify work

Requires tasks:write. Reopens failed/expired Amplify components on the original account/worker; approved work stays completed. Worker business hours and dispatch availability apply. No new credit is charged. Standalone comments follow their existing automatic replacement policy.

**Parameters**
- `taskId` (path, required): string

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 429 Error

### GET /v2/openapi.json

OpenAPI document

**Responses:** 200 Success

### POST /v2/uploads

Create an upload

Videos must be MP4 or MOV with H.264 or HEVC (H.265) video and AAC audio when present, no larger than 200 MB or 3 minutes, up to 2160 × 3840 and 60 fps. Supported uploads are stored without transcoding.

**JSON body**
- Variant 1:
  - `kind`: video | image (required)
  - `filename`: string (required) [minLength=1, maxLength=255] — For video uploads, the filename must use the .mp4 or .mov extension.
  - `contentType`: string (required) [minLength=1, maxLength=100] — For video uploads, use video/mp4 for MP4 or video/quicktime for MOV.
  - `sizeBytes`: integer (required) [exclusiveMinimum=0] — Video uploads may be at most 209,715,200 bytes (200 MiB); images may be at most 52,428,800 bytes (50 MiB).
- Variant 2:
  - `kind`: video | image (required)
  - `sourceUrl`: string (required) [maxLength=2048, format="uri"]
  - `filename`: string [minLength=1, maxLength=255]

**Responses:** 201 Success; 400 Error; 401 Error; 403 Error; 413 Error; 415 Error; 422 Error; 429 Error

### POST /v2/uploads/{uploadId}/complete

Complete a signed upload

Validates uploaded video container, codecs, size, duration, dimensions, and frame rate before marking it ready.

**Parameters**
- `uploadId` (path, required): string

**Responses:** 200 Success; 409 Error; 413 Error; 415 Error; 422 Error

### GET /v2/uploads/{uploadId}

Get upload status

**Parameters**
- `uploadId` (path, required): string

**Responses:** 200 Success; 404 Error

### POST /v2/posts

Create and queue a post

Optionally assign up to 10 tags or template reporting groups with contentGroupIds. Tags are for internal analytics and do not appear in the social caption.

**Parameters**
- `Idempotency-Key` (header, optional): string

**JSON body**
- `media`: array<string> (required) [minItems=1, maxItems=20]
- `accountIds`: array<string> (required) [minItems=1, maxItems=2] — Choose at most one TikTok account and one Instagram account; duplicate platforms are rejected.
- `skipApproval`: boolean — When true, send the post directly to the posting queue instead of waiting for dashboard approval.
- `caption`: string [maxLength=2200]
- `hashtags`: array<string> [maxItems=30]
- `sound`: object
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string [maxLength=2000]
- `priority`: integer [minimum=0]
- `scheduledAfter`: string [format="date-time"] — Earliest release instant, expressed as ISO 8601 with a timezone offset. Actual publishing follows worker-local windows and capacity; this is not an exact posting time.
- `expiresAt`: string [format="date-time"] — Stop releasing queued targets at this instant. Does not recall work already released to an operator.
- `contentGroupIds`: array<string> [maxItems=10] — Optional tag or template reporting-group IDs. Use the contentGroupId returned by POST /v2/content-tags. Multiple groups are allowed; omit this field for no explicit tags.
- `instagramPostFormat`: reel | post — How Instagram should publish the media. Regular posts require a company capability; Reels are the default.
- `slides`: array<object> [minItems=2, maxItems=20]
  - `image`: string (required) [minLength=1, maxLength=200]
  - `textElements`: array<object> (required) [maxItems=6]
    - `content`: string (required) [minLength=1, maxLength=500]
    - `style`: object (required)
      - `backgroundColor`: none | black | white | red | yellow | blue [default="none"]
      - `textColor`: white | black | red | yellow | blue [default="white"]
      - `fontSize`: number [minimum=6, maximum=72, default=16]
      - `fontWeight`: normal | bold [default="normal"]
      - `textAlign`: left | center | right [default="center"]
      - `textMode`: stroke | background | plain | snapchat [default="stroke"]
      - `positionX`: number [minimum=0, maximum=100, default=50]
      - `positionY`: number (required) [minimum=0, maximum=100]
      - `width`: number [minimum=20, maximum=100, default=88]
      - `padding`: number [minimum=0, maximum=20, default=0]
  - `imageFit`: fit | fill [default="fill"]
  - `role`: hook | middle | cta

**Responses:** 200 Idempotent replay; 201 Post created; 400 Error; 409 Error; 429 Error

### GET /v2/posts

List posts

Optionally filter by one tag or template reporting group with contentGroupId, or use `untagged` to return posts with no attribution.

**Parameters**
- `limit` (query, optional): integer
- `cursor` (query, optional): string
- `accountId` (query, optional): string
- `status` (query, optional): draft | pending_approval | queued | posting | posted | partially_posted | cancelled | processing | failed
- `since` (query, optional): string
- `contentGroupId` (query, optional): string or untagged — Return only posts attributed to this tag or template reporting group. Use `untagged` for posts with no tag or template attribution.

**Responses:** 200 Success

### POST /v2/posts/{postId}/queue

Queue an existing draft post

Adds one queue item per requested account to an existing post whose media is ready.

**Parameters**
- `postId` (path, required): string
- `Idempotency-Key` (header, optional): string

**JSON body**
- `accountIds`: array<string> (required) [minItems=1, maxItems=2] — Choose at most one TikTok account and one Instagram account; duplicate platforms are rejected.
- `skipApproval`: boolean — When true, send the post directly to the posting queue instead of waiting for dashboard approval.
- `caption`: string [maxLength=2200]
- `hashtags`: array<string> [maxItems=30]
- `sound`: object
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string [maxLength=2000]
- `priority`: integer [minimum=0]
- `scheduledAfter`: string [format="date-time"] — Earliest release instant, expressed as ISO 8601 with a timezone offset. Actual publishing follows worker-local windows and capacity; this is not an exact posting time.
- `expiresAt`: string [format="date-time"] — Stop releasing queued targets at this instant. Does not recall work already released to an operator.
- `instagramPostFormat`: reel | post — How Instagram should publish the media. Regular posts require a company capability; Reels are the default.

**Responses:** 200 Idempotent replay; 201 Post queued; 400 Error; 404 Error; 409 Conflict (`media_not_ready`, `duplicate_queue_item`, or post already published); 429 Error

### GET /v2/posts/{postId}

Get a post

**Parameters**
- `postId` (path, required): string

**Responses:** 200 Success; 404 Error

### PATCH /v2/posts/{postId}

Update post metadata or content tags

contentGroupIds replaces the complete explicit assignment list and can be changed before or after publishing. Resend the desired list to add or remove individual tags; send [] to remove all explicit tags. Other metadata remains subject to normal post editability rules.

**Parameters**
- `postId` (path, required): string

**JSON body**
- `caption`: string [maxLength=2200]
- `hashtags`: array<string> [maxItems=30]
- `sound`: object
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string | null [maxLength=2000]
- `contentGroupIds`: array<string> [maxItems=10] — Replace the post's complete list of explicitly assigned tag or template reporting groups. This remains editable after publishing. To remove one tag, resend the list without it; pass [] to remove every explicit tag. Template provenance inherited from the post's original template remains visible.
- `instagramPostFormat`: reel | post — How Instagram should publish the media. `post` is rejected unless the company has regular feed posts enabled.

**Responses:** 200 Success; 409 Error

### DELETE /v2/posts/{postId}

Cancel a post

**Parameters**
- `postId` (path, required): string
- `accountId` (query, optional): string

**Responses:** 200 Success; 409 Error

### GET /v2/content-tags

List content tags

Returns active and archived custom tags with the stable contentGroupId value used when creating, updating, or filtering posts.

**Responses:** 200 Success

### POST /v2/content-tags

Create a content tag

Creates an optional company-scoped analytics tag. Use the returned contentGroupId in a post's contentGroupIds array.

**JSON body**
- `name`: string (required) [minLength=1, maxLength=50] — Unique tag name within the API key's company.
- `color`: slate | blue | cyan | green | amber | orange | rose | violet [default="slate"] — Dashboard display color. Defaults to slate.

**Responses:** 201 Success; 409 Error

### PATCH /v2/content-tags/{tagId}

Update or archive a content tag

Rename or recolor a tag, archive it to prevent new assignments, or restore it with archived: false. Archiving preserves existing post attribution and analytics.

**Parameters**
- `tagId` (path, required): string

**JSON body**
- `name`: string [minLength=1, maxLength=50] — New unique tag name.
- `color`: slate | blue | cyan | green | amber | orange | rose | violet — New dashboard display color.
- `archived`: boolean — Set true to hide the tag from new assignments without changing historical attribution; set false to restore it.

**Responses:** 200 Success; 404 Error; 409 Error

### DELETE /v2/content-tags/{tagId}

Delete a content tag

Permanently deletes the tag and removes it from all posts in the company. Prefer archiving for historical continuity.

**Parameters**
- `tagId` (path, required): string

**Responses:** 200 Success; 404 Error

### GET /v2/schedule

Get the company posting schedule

Requires accounts:read. Returns the default for the API key's company. Individual account overrides take precedence.

**Responses:** 200 Success; 403 Error; 404 Error

### PATCH /v2/schedule

Set the company posting schedule

Requires accounts:write. Replaces the company default, preserving individual account overrides. Individual account schedules still take priority even when this default is edited afterward through the dashboard, API, or MCP. One daily post per selected 3-hour window, in each assigned worker's local timezone. Send schedule: null to restore global rules, not pause posting. Applies to future dispatches; already released tasks are unchanged. Missed posts may use unused make-up windows, up to 4 posts/day/account.

**JSON body**
- `schedule`: object | null (required) — Replace the active windows with 1–4 sorted HH:mm starts, at least 3 hours apart, between 08:00 and 17:00 inclusive. Standard slots: 08:00, 11:00, 14:00, 17:00. Times follow each assigned worker's timezone. null clears the override; it does not pause posting.
  - `windows`: array<string> (required) [minItems=1, maxItems=4]

**Responses:** 200 Success; 400 Error; 403 Error; 404 Error

### GET /v2/accounts/{accountId}/schedule

Get an account's effective posting schedule

Requires accounts:read. Returns the account override, company default, effective schedule, source, and timezone. Account must belong to the API key's company.

**Parameters**
- `accountId` (path, required): string

**Responses:** 200 Success; 403 Error; 404 Error

### PATCH /v2/accounts/{accountId}/schedule

Set or clear an account posting schedule

Requires accounts:write. Replaces this account's override. Send schedule: null to inherit the company default (or global rules), not pause posting. Applies to future dispatches; already released tasks are unchanged. Times follow the assigned worker's timezone. Missed posts may use unused make-up windows, up to 4 posts/day.

**Parameters**
- `accountId` (path, required): string

**JSON body**
- `schedule`: object | null (required) — Replace the active windows with 1–4 sorted HH:mm starts, at least 3 hours apart, between 08:00 and 17:00 inclusive. Standard slots: 08:00, 11:00, 14:00, 17:00. Times follow each assigned worker's timezone. null clears the override; it does not pause posting.
  - `windows`: array<string> (required) [minItems=1, maxItems=4]

**Responses:** 200 Success; 400 Error; 403 Error; 404 Error

### GET /v2/accounts

List publishing accounts

Requires accounts:read. Returns only the API key's company accounts. Includes stored profilePictureUrl, bio, and warmupSearchTerms. Missing picture/bio values are null; missing terms are []. Setup placeholders contain requested values; pending profile edits retain previous values until worker approval. Stored values do not independently verify the live platform profile or completed warm-up.

**Parameters**
- `platform` (query, optional): tiktok | instagram

**Responses:** 200 Success

### POST /v2/accounts

Create a publishing account

Provide 1–5 warmup.searchTerms. Requests with more than five terms are rejected. Existing accounts and tasks with longer lists remain supported; new or edited lists must have at most five terms. The response uses warmupSearchTerms for the stored terms. Account setup is asynchronous. Includes stored profilePictureUrl, bio, and warmupSearchTerms. Missing picture/bio values are null; missing terms are []. Setup placeholders contain requested values; pending profile edits retain previous values until worker approval. Stored values do not independently verify the live platform profile or completed warm-up.

**JSON body**
- `platform`: tiktok | instagram (required)
- `username`: string (required)
- `displayName`: string (required) [minLength=1, maxLength=80]
- `bio`: string (required) [minLength=1, maxLength=220]
- `profilePictureUrl`: string (required) [maxLength=2048, format="uri"]
- `bioUrl`: string [maxLength=2048, format="uri"]
- `warmup`: object (required)
  - `searchTerms`: array<string> (required) [minItems=1, maxItems=5] — 1–5 account-specific warm-up search terms. Existing accounts may contain more than five.

**Responses:** 201 Setup requested; 400 Error; 403 Error; 409 Error; 429 Error

### GET /v2/accounts/{accountId}

Get a publishing account

Requires accounts:read. Account must belong to the API key's company. Includes stored profilePictureUrl, bio, and warmupSearchTerms. Missing picture/bio values are null; missing terms are []. Setup placeholders contain requested values; pending profile edits retain previous values until worker approval. Stored values do not independently verify the live platform profile or completed warm-up.

**Parameters**
- `accountId` (path, required): string

**Responses:** 200 Success; 404 Error

### GET /v2/accounts/{accountId}/queue

Get an account queue

**Parameters**
- `accountId` (path, required): string
- `limit` (query, optional): integer
- `cursor` (query, optional): string

**Responses:** 200 Success

### GET /v2/posts/{postId}/analytics

Get post analytics

**Parameters**
- `postId` (path, required): string
- `limit` (query, optional): integer
- `cursor` (query, optional): string

**Responses:** 200 Success

### GET /v2/accounts/{accountId}/analytics

Get account analytics

**Parameters**
- `accountId` (path, required): string

**Responses:** 200 Success

### GET /v2/usage

Get plan and API usage

**Responses:** 200 Success

### GET /v2/workspace

Read a company workspace

Returns only the company bound to the API key, its connected accounts, and sanitized image folder and pack metadata. Requires workspace:read.

**Responses:** 200 Success; 401 Error; 403 Error; 404 Error

### POST /v2/workspace/media-urls

Create short-lived image download URLs

Resolves selected images through company-owned folder records before signing. Storage paths and credentials are never accepted from the caller. Requires media:read.

**JSON body**
- `items`: array<object> (required) [minItems=1, maxItems=100]
  - `folderId`: string (required) [minLength=1, maxLength=200]
  - `imageId`: string (required) [minLength=1, maxLength=200]
- `expiresInSeconds`: integer [minimum=60, maximum=3600, default=900]

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error

### GET /v2/context

Read company, scopes, and capabilities

Confirms the company bound to this key, granted scopes, available tools and capabilities.

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

### POST /v2/uploads/batch

Import an ordered resumable URL batch

Requires posts:write. Imports 1–100 public URLs durably; job results retain input indices and per-file failures. Poll /v2/jobs/{jobId}. Reuse a key only with identical input.

**Parameters**
- `Idempotency-Key` (header, optional): string — Company-scoped operation key. Reuse with identical payload only; conflicting use returns 409.

**JSON body**
- `items`: array<object> (required) [minItems=1, maxItems=100]
  - `kind`: video | image (required)
  - `sourceUrl`: string (required) [maxLength=2048, format="uri"]
  - `filename`: string [minLength=1, maxLength=255]

**Responses:** 202 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

### GET /v2/jobs/{jobId}

Read job progress and results

Requires analytics:read for reports; posts:write for previews/imports. Poll persisted progress or read final ordered results, preview URLs, or report. Requires the job's scope and the same company.

**Parameters**
- `jobId` (path, required): string

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

### POST /v2/analytics/reports

Compare UTC-day growth and rank observed destinations

Requires analytics:read. Requires ready V3 rollups. Dates must be UTC midnight: from inclusive, to exclusive, maximum 366 days; previous period has equal duration. Growth follows collection dates. Top posts use latest cumulative views for destinations whose collection spans overlap the period. Includes freshness/coverage and up to 500 groups; tag groups overlap. Poll the returned job.

**Parameters**
- `Idempotency-Key` (header, optional): string — Company-scoped operation key. Reuse with identical payload only; conflicting use returns 409.

**JSON body**
- `from`: string (required) [format="date-time"]
- `to`: string (required) [format="date-time"]
- `comparePrevious`: boolean [default=true]
- `accountId`: string [minLength=1, maxLength=200]
- `platform`: tiktok | instagram
- `contentGroupId`: string [maxLength=240]
- `groupBy`: account | platform | content_group | template [default="account"]
- `topLimit`: integer [minimum=1, maximum=100, default=10]

**Responses:** 202 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

### POST /v2/drafts

Create an unqueued editable draft

Requires posts:write. Creates an unqueued draft. Optional slides retain editable text and styles; finished artwork can be used as-is. Posting billing is enforced on submission. Preview before queueing; draft creation counts once toward the post creation cap.

**Parameters**
- `Idempotency-Key` (header, optional): string — Company-scoped operation key. Reuse with identical payload only; conflicting use returns 409.

**JSON body**
- `media`: array<string> (required) [minItems=1, maxItems=20]
- `caption`: string [maxLength=2200]
- `hashtags`: array<string> [maxItems=30]
- `sound`: object
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string [maxLength=2000]
- `contentGroupIds`: array<string> [maxItems=10] — Optional tag or template reporting-group IDs. Use the contentGroupId returned by POST /v2/content-tags. Multiple groups are allowed; omit this field for no explicit tags.
- `instagramPostFormat`: reel | post — How Instagram should publish the media. Regular posts require a company capability; Reels are the default.
- `slides`: array<object> [minItems=2, maxItems=20]
  - `image`: string (required) [minLength=1, maxLength=200]
  - `textElements`: array<object> (required) [maxItems=6]
    - `content`: string (required) [minLength=1, maxLength=500]
    - `style`: object (required)
      - `backgroundColor`: none | black | white | red | yellow | blue [default="none"]
      - `textColor`: white | black | red | yellow | blue [default="white"]
      - `fontSize`: number [minimum=6, maximum=72, default=16]
      - `fontWeight`: normal | bold [default="normal"]
      - `textAlign`: left | center | right [default="center"]
      - `textMode`: stroke | background | plain | snapchat [default="stroke"]
      - `positionX`: number [minimum=0, maximum=100, default=50]
      - `positionY`: number (required) [minimum=0, maximum=100]
      - `width`: number [minimum=20, maximum=100, default=88]
      - `padding`: number [minimum=0, maximum=20, default=0]
  - `imageFit`: fit | fill [default="fill"]
  - `role`: hook | middle | cta

**Responses:** 200 Idempotent replay; 201 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

### PATCH /v2/drafts/{postId}

Edit draft artwork or metadata with version conflict protection

Requires posts:write. Edits only an unqueued draft. expectedVersionId prevents overwriting newer artwork. Replacing slides invalidates the preview; request a new preview before submission.

**Parameters**
- `postId` (path, required): string

**JSON body**
- `media`: array<string> [minItems=1, maxItems=20]
- `caption`: string [maxLength=2200]
- `hashtags`: array<string> [maxItems=30]
- `sound`: object
  - `mode`: trending | original | specific (required)
  - `url`: string [format="uri"]
  - `urlsByPlatform`: object
    - `tiktok`: string [format="uri"]
    - `instagram`: string [format="uri"]
- `notes`: string [maxLength=2000]
- `contentGroupIds`: array<string> [maxItems=10] — Optional tag or template reporting-group IDs. Use the contentGroupId returned by POST /v2/content-tags. Multiple groups are allowed; omit this field for no explicit tags.
- `instagramPostFormat`: reel | post — How Instagram should publish the media. Regular posts require a company capability; Reels are the default.
- `slides`: array<object> [minItems=2, maxItems=20]
  - `image`: string (required) [minLength=1, maxLength=200]
  - `textElements`: array<object> (required) [maxItems=6]
    - `content`: string (required) [minLength=1, maxLength=500]
    - `style`: object (required)
      - `backgroundColor`: none | black | white | red | yellow | blue [default="none"]
      - `textColor`: white | black | red | yellow | blue [default="white"]
      - `fontSize`: number [minimum=6, maximum=72, default=16]
      - `fontWeight`: normal | bold [default="normal"]
      - `textAlign`: left | center | right [default="center"]
      - `textMode`: stroke | background | plain | snapchat [default="stroke"]
      - `positionX`: number [minimum=0, maximum=100, default=50]
      - `positionY`: number (required) [minimum=0, maximum=100]
      - `width`: number [minimum=20, maximum=100, default=88]
      - `padding`: number [minimum=0, maximum=20, default=0]
  - `imageFit`: fit | fill [default="fill"]
  - `role`: hook | middle | cta
- `expectedVersionId`: string [minLength=1, maxLength=200] — Reject a stale artwork edit rather than overwriting another editor's version.

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

### GET /v2/posts/{postId}/content

Read media, slide text, styles, and version

Requires posts:read. Returns slide text/styles, upload IDs, signed background URLs (15 minutes), and the current versionId.

**Parameters**
- `postId` (path, required): string

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

### POST /v2/posts/{postId}/preview

Render current draft without approving or submitting it

Requires posts:write. Reads or requests the current draft preview. Rendering is asynchronous and does not approve or queue the draft. Old renders cannot replace newer versions.

**Parameters**
- `postId` (path, required): string
- `Idempotency-Key` (header, optional): string — Company-scoped operation key. Reuse with identical payload only; conflicting use returns 409.

**Responses:** 202 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

### GET /v2/posts/{postId}/preview

Read current rendered preview

Requires posts:read. Reads or requests the current draft preview. Rendering is asynchronous and does not approve or queue the draft. Old renders cannot replace newer versions.

**Parameters**
- `postId` (path, required): string

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

### GET /v2/workspace/folders

Browse company folders with bounded search and cursor

Requires workspace:read. Browse company-owned folders with optional search. Scans are bounded; continue with nextCursor even if a filtered page is empty.

**Parameters**
- `limit` (query, optional): integer
- `cursor` (query, optional): string
- `search` (query, optional): string

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

### GET /v2/workspace/folders/{folderId}/images

Browse company-owned folder images

Requires workspace:read. Browse available images in a company-owned folder with optional label, description, or tag search. Continue with nextCursor even if a filtered page is empty.

**Parameters**
- `folderId` (path, required): string
- `limit` (query, optional): integer
- `cursor` (query, optional): string
- `search` (query, optional): string

**Responses:** 200 Success; 400 Error; 401 Error; 403 Error; 404 Error; 409 Error; 422 Error; 429 Error; 503 Error

## MCP

Connect an MCP client to https://us-central1-clickbaitcarousel.cloudfunctions.net/mcp using Streamable HTTP. Send `Authorization: Bearer YOUR_API_KEY` on every request.

### Claude Code

Register HumanPost as a user-level Streamable HTTP server.

```bash
claude mcp add --scope user --transport http humanpost https://us-central1-clickbaitcarousel.cloudfunctions.net/mcp --header "Authorization: Bearer YOUR_API_KEY"
```

### Claude (web/desktop)

Add HumanPost as a custom connector in Claude.

1. Open Settings, then Connectors.
2. Choose Add custom connector and paste the MCP URL.
3. Under Request headers, add Authorization with the value Bearer followed by your API key.

```text
URL: https://us-central1-clickbaitcarousel.cloudfunctions.net/mcp
Authorization: Bearer YOUR_API_KEY
```

### Cursor

Add this server entry to your Cursor mcp.json file.

```json
{
  "mcpServers": {
    "humanpost": {
      "url": "https://us-central1-clickbaitcarousel.cloudfunctions.net/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_KEY"
      }
    }
  }
}
```

### Any client

Use Streamable HTTP transport and send the API key on every request.

```json
{
  "name": "humanpost",
  "transport": {
    "type": "streamable-http",
    "url": "https://us-central1-clickbaitcarousel.cloudfunctions.net/mcp"
  },
  "headers": {
    "Authorization": "Bearer YOUR_API_KEY"
  }
}
```

### MCP tools

| Tool | What it does | Required scope |
| --- | --- | --- |
| `list_task_types` | Discover comment and Amplify inputs, credit costs, availability and blockers. Optionally inspect a company account. Worker business hours apply; review remains with HumanPost. Requires tasks:read. | `tasks:read` |
| `create_task` | Create an exact Comment or one Amplify request containing Repost + Like and Comment. Requires idempotencyKey; keep payload unchanged for retries. No worker or payout overrides. Creation reserves one credit. Requires tasks:write. | `tasks:write` |
| `list_tasks` | List company-visible requests and effective status. Operational Amplify children and comment replacements are embedded in the original task. Follow nextCursor even after an empty filtered page. Requires tasks:read. | `tasks:read` |
| `get_task` | Read combined Amplify progress or the full bounded comment attempt chain under one stable task ID. Completed means approved; submitted screenshots await review. Requires tasks:read. | `tasks:read` |
| `list_task_events` | Page persisted task revisions; events arrive asynchronously. historyComplete is false for legacy tasks; missing history is not invented. Requires tasks:read. | `tasks:read` |
| `get_task_evidence` | Page current and archived screenshots with expiring signed URLs. Requires company task ownership. URLs are bearer links until expiry; no worker personal data is included. Requires tasks:evidence:read. | `tasks:evidence:read` |
| `cancel_task` | Cancel unfinished Comment or Amplify work while retaining evidence/history. Cancellation cannot undo platform actions. Amplify refunds only before either component is approved. Requires tasks:write. | `tasks:write` |
| `resume_task` | Resume failed/expired components of an unfinished Amplify bundle on the original account and worker. Approved components stay complete; no additional credit is charged. Requires tasks:write. | `tasks:write` |
| `get_context` | Call first to confirm the key's company, scopes, available tools, and publishing capabilities. | `authenticated context / job scope` |
| `list_accounts` | Inspect account IDs, eligibility, daily usage, queue depth, and stored profilePictureUrl, bio, and warmupSearchTerms before choosing targets. Setup values may be requested; stored values do not independently verify the live platform profile or completed warm-up. One account per platform per post. Requires accounts:read. | `accounts:read` |
| `get_account` | Read one company-owned publishing account, including stored profilePictureUrl, bio, and warmupSearchTerms. Setup values may be requested; pending profile edits retain previous values until worker approval. This does not independently verify the live platform profile or completed warm-up. Requires accounts:read. | `accounts:read` |
| `get_company_schedule` | Read the company's daily posting windows; account overrides take precedence. Requires accounts:read. | `accounts:read` |
| `update_company_schedule` | Replace company windows while preserving account overrides. Example: {schedule:{windows:['08:00','17:00']}}. null restores global rules, not a pause; future dispatches only, in worker-local time, up to four posts/day including make-up posts. Requires accounts:write. | `accounts:write` |
| `get_account_schedule` | Read the override, company default, effective windows, source, and worker timezone. Requires accounts:read. | `accounts:read` |
| `update_account_schedule` | Replace an account's daily worker-local windows. schedule:null restores company inheritance, not a pause. Future dispatches only, up to four posts/day including make-up posts. Requires accounts:write. | `accounts:write` |
| `get_account_queue` | Page through queued targets. Pass nextCursor back as cursor, even after an empty page. scheduledAfter is an earliest release instant, not a guaranteed posting time. Requires posts:read. | `posts:read` |
| `create_account` | Submit a complete TikTok or Instagram profile to the worker setup pool. Account setup is asynchronous. Requires accounts:write. | `accounts:write` |
| `upload_media` | Import a public sourceUrl (auto-completed), or provide filename, contentType and actual sizeBytes for a signed PUT. After PUT with requiredHeaders, call complete_upload. Videos: MP4/MOV, H.264/HEVC, AAC, <=200MB, <=3 minutes, <=2160x3840, <=60fps; images: JPG/PNG/WebP/HEIC <=50MB. Supported video is not transcoded. Requires posts:write. | `posts:write` |
| `get_upload_status` | Inspect pending, ready, or failed uploads and corrective errors. Requires posts:read. | `posts:read` |
| `complete_upload` | Validate media after the signed PUT finishes. Ready uploads replay safely. Requires posts:write. | `posts:write` |
| `create_upload_batch` | Start a resumable, ordered import of up to 100 public media URLs. Poll get_job; results retain input indices and per-file errors. Use an idempotencyKey for safe retries. Requires posts:write. | `posts:write` |
| `get_job` | Poll a batch upload, draft preview, or analytics report. Access requires the job's scope. Completed jobs return their ordered results or report. | `authenticated context / job scope` |
| `create_post` | Create one-video or 2–20-image post and submit it to up to one account per platform. Managed approval rules apply unless skipApproval is explicitly requested. For review first, use create_draft. Use idempotencyKey for safe retries; scheduledAfter/expiresAt define a release interval, not exact publication times. Requires posts:write. | `posts:write` |
| `create_draft` | Prepare a video or carousel without queueing or publishing it. Optional slides keep text editable. Use idempotencyKey for safe retries, then inspect content and request a preview before queue_post. Requires posts:write. | `posts:write` |
| `get_post_content` | Read media URLs, slide text/styles, and versionId. Background image URLs expire in 15 minutes. Requires posts:read. | `posts:read` |
| `update_draft` | Replace draft media or slides, or edit metadata. expectedVersionId detects conflicting artwork edits. Submitted or published posts are rejected. Preview the new version before submitting it. Requires posts:write. | `posts:write` |
| `request_post_preview` | Start a durable render of the current unqueued draft version without approving or queueing it. Poll get_job or get_post_preview. Old renders cannot replace newer artwork. Requires posts:write. | `posts:write` |
| `get_post_preview` | Read the current version's preview URLs and pending/ready/failed status. A preview does not submit a draft. Requires posts:read. | `posts:read` |
| `queue_post` | Queue an existing draft with ready media to at most one account per platform. No media creation. Managed approval rules apply unless skipApproval is explicitly requested. Use idempotencyKey for safe retries; scheduledAfter/expiresAt define a release interval. Requires posts:write. | `posts:write` |
| `get_post_status` | Read aggregate and per-account publishing status, release times, and posted URLs. Requires posts:read. | `posts:read` |
| `list_posts` | Page through company posts filtered by account, status, creation date, or content group. Pass nextCursor as cursor, even if a filtered page has no results. Requires posts:read. | `posts:read` |
| `update_post` | Edit metadata while every target remains editable. contentGroupIds replaces explicit tags and remains editable after publication; [] clears explicit tags while preserving template provenance. Requires posts:write. | `posts:write` |
| `list_content_tags` | Read active and archived company tags and stable contentGroupId values. Requires posts:read. | `posts:read` |
| `create_content_tag` | Create an optional internal reporting tag; it does not change public captions. Requires posts:write. | `posts:write` |
| `update_content_tag` | Rename, recolor, archive, or restore a tag. Archiving preserves attribution. Requires posts:write. | `posts:write` |
| `delete_content_tag` | Remove a custom tag from all company posts. Archive instead to preserve history. Requires posts:write. | `posts:write` |
| `cancel_post` | Cancel pre-release targets and report targets already posting, posted, or cancelled as skipped. Published social posts are not deleted. Requires posts:write. | `posts:write` |
| `get_post_analytics` | Read the latest summary and paginate historical destination observations. Inspect lastUpdated/collectedAt for freshness. Requires analytics:read. | `analytics:read` |
| `get_account_analytics` | Read legacy latest-200-post totals with scannedPosts and coverage metadata; use create_analytics_report for complete rollup comparisons. Requires analytics:read. | `analytics:read` |
| `create_analytics_report` | Create a durable company-scoped report of UTC-day growth, previous-period comparison, account/platform/tag/template breakdowns, and top destinations by latest cumulative views. Poll get_job. Requires active V3 rollups; availability and freshness are explicit. Requires analytics:read. | `analytics:read` |
| `get_usage` | Read effective daily caps, counters, plan credits, and the next UTC reset. Requires usage:read. | `usage:read` |
| `get_workspace` | Read company/account context. includeAssets:true adds a legacy bounded folder/image-pack snapshot; use paginated folder/image tools for efficient complete asset discovery. Requires workspace:read. | `workspace:read` |
| `list_workspace_folders` | Page through company folders with optional name search. Follow nextCursor even after an empty search page. Requires workspace:read. | `workspace:read` |
| `list_workspace_images` | Page through available images in a company-owned folder, optionally searching labels, descriptions, or tags. Requires workspace:read. | `workspace:read` |
| `get_workspace_media_urls` | Resolve company-owned images to signed download URLs valid for 60–3600 seconds. Caller-supplied storage paths are never accepted. Requires media:read. | `media:read` |

The `create_post` tool accepts 2–20 image upload IDs for a carousel. Their order is the slide order.

### Content tags with MCP

```text
# Create a reusable company tag; keep the returned contentGroupId.
create_content_tag({ name: "Launch", color: "blue" })
→ { contentGroupId: "tag:abc123", ... }

# Add it when creating a post.
create_post({ media: ["UPLOAD_ID"], accountIds: ["ACCOUNT_ID"], contentGroupIds: ["tag:abc123"] })

# Replace the post's tags at any time, including after publishing.
update_post({ postId: "POST_ID", contentGroupIds: ["tag:abc123", "tag:def456"] })

# Remove every explicit tag.
update_post({ postId: "POST_ID", contentGroupIds: [] })
```
