# MCP server: drive Pamba from Claude Code, Codex or any MCP client

> Connect an MCP client to your workspace, pick the toolsets you need, and learn how calls, credits, waits, approvals and hand-offs behave before you automate anything.

Pamba's MCP server exposes every tool the in-app assistant has, over the Model Context Protocol, at one endpoint: `https://api.pamba.app/mcp`. Your client's model gets the same product knowledge the assistant runs on, so it can chain the tools into whole workflows (research a format, build the avatar, create the projects, generate, schedule) without you translating screens into API calls. This guide covers what the reference cannot: how the surface is cut, how a call behaves, what waits, what costs credits, and the ordering rules that trip people up. The exact tool list, with every parameter, is the [MCP tool reference](/help/mcp-tool-reference).

## Connect from a chat app

Choose your chat app in the [visual setup guide](/connect/setup): [Claude](/connect/setup?app=claude), [ChatGPT](/connect/setup?app=chatgpt), [Grok](/connect/setup?app=grok) or [Gemini](/connect/setup?app=gemini). Each walkthrough shows the provider’s menus, where to paste the URL, how to approve access on Pamba and a read-only prompt to try. Account requirements and links to the provider’s current instructions are included.

In a chat app that supports remote MCP with OAuth, add `https://api.pamba.app/mcp` as a custom connector. Pamba opens an approval page: sign in and choose one or more workspaces, or enter your workspace API key on that Pamba page. Review the requesting app's return address and approve. Never paste the key into a chat message.

Find the URL in **Settings → Connected apps** and choose **Copy URL**. The same setup instructions and URL are available on [Connected apps](/connect), where you can also disconnect apps later.

If signing in takes you to Pamba's onboarding flow, finish that setup, then return to your chat app and connect Pamba again. The original connection request does not resume automatically. You need an existing workspace to approve access.

OAuth connections get every tool an API-key connection has: projects, avatars and generation, scheduling and publishing posts, campaigns, social accounts, workspace settings and members, billing checkouts and API keys. They act in the workspaces you approved. Tool calls run immediately; your chat app controls any per-action confirmation. URL filters can narrow a connection but cannot expand it.

Beyond the approved workspaces, the app can read the name and company details of every workspace you belong to, create workspaces, accept workspace invitations, list or revoke your API keys in any workspace, disconnect your other connected apps, and change your notification settings.

What the app sets up keeps running after you disconnect it or the connection ends: posts it schedules still publish, campaigns it turns on keep spending credits, and API keys, invite links and members it adds keep working until you turn them off or remove them in Pamba.

Each authorization lasts up to 90 days. Access tokens expire after one hour and the client refreshes them automatically; reconnect when the authorization expires. Disconnect anytime from [Connected apps](/connect). Losing membership or deleting a workspace stops access to that workspace. Revoking a source API key stops the connection. Disconnecting stops the entire connection in every approved workspace, including queued calls that have not started; it does not cancel generation already running. Disconnecting an app does not revoke an existing API key.

One connection can cover up to 50 workspaces you explicitly select at approval. Ask your chat app to use a workspace by name. It calls `list_connected_workspaces` to find approved IDs, then passes `workspace_id` on each action. A multi-workspace grant requires that ID; the server never silently switches workspaces. Projects, avatars and credit charges stay within the selected workspace. New memberships and workspaces are not added automatically—reconnect to approve a different selection. Existing connections keep their original workspace. API-key approval remains limited to that key’s workspace.

Pamba staff accounts cannot connect a chat app; staff connect Claude Code or Codex with a workspace API key instead.

Availability and setup menus depend on the chat provider, account and administrator settings. A supported connector must be installed; an API key pasted into regular chat does not give the model an API tool. The API-key setup for Claude Code and Codex below continues to work.

## Connect using an API key

- **Endpoint:** `https://api.pamba.app/mcp`, Streamable HTTP, one JSON-RPC message per POST.
- **Credential:** an `X-API-Key` header with a workspace API key. Create one in workspace settings (open the workspace switcher, then Settings, then API keys); the secret is shown once. A key belongs to exactly one workspace and every call runs there; sending an `X-Workspace-Id` header that names a different workspace is refused. To work in another workspace, connect with that workspace's key. A key can make one for a workspace it created itself: `create_workspace` over the key, then `create_api_key` with `key_workspace_id` set to the new workspace's id, sets up a new workspace and gives its key without opening the app; it never makes keys for the person's other workspaces. Three tools act on the person behind the key rather than the connected workspace, because that is what they do in the app too: `delete_workspace` accepts any workspace the person administers, and `list_api_keys` and `revoke_api_key` cover their keys in every workspace. The instructions tell the model to confirm with the person before the destructive two. Deleting the person's own Pamba account is not available over MCP, to the in-app assistant or with an API key: only the person can delete it, in the app's Settings. An `Authorization: Bearer <JWT>` plus `X-Workspace-Id` header works too, the same way it does on the REST API.

Claude Code:

```
claude mcp add --transport http pamba https://api.pamba.app/mcp --header "X-API-Key: <your workspace API key>"
```

Codex (`~/.codex/config.toml` or the project's `.codex/config.toml`):

```
[mcp_servers.pamba]
url = "https://api.pamba.app/mcp"
http_headers = { "X-API-Key" = "<your workspace API key>" }
```

Any client that takes a JSON server list:

```
{
  "mcpServers": {
    "pamba": {
      "type": "http",
      "url": "https://api.pamba.app/mcp",
      "headers": { "X-API-Key": "<your workspace API key>" }
    }
  }
}
```

Once connected, ask the model "What can you do with Pamba?": the answer comes from the product map the server sends at connection, not from guesses.

## Choose how much of the surface to load

The full surface is about four hundred tools. Most clients work best with fewer in context, and a script that only reads analytics has no reason to see the tools that spend money. Three switches on the endpoint URL shape a connection: two cut the surface, one caps spend.

- `?toolsets=videos,avatars` loads only the named toolsets. The names: `videos` (create, generate, replicate, drafts, scheduling, campaigns, the avatar wizard, and `list_avatars`, because every project starts from an avatar id), `editing` (the editor: concepts, takes, renders, text hooks, captions, model switches), `avatars` (appearance, voices, scene libraries, image editing), `accounts` (the account fleet, seats and billing, profile edits, warming, health, comments, individual scheduled posts, and `upload_editor_media` with `create_draft_project` for the project it files under, because a profile picture or a post's media must be a Pamba URL first), `ideas` (the ideas library and tracked inspiration accounts), `discover` (the viral corpus, niche feed, trends, the source picker), `content` (script batches and their drafts, knowledge base, writer engine, learning loop), `automation` (campaign strategy and content plans), `workspace` (settings, members, onboarding, billing and credits, the media library, API keys, help, support).
- `?readonly=true` loads only read-only tools. A tool outside the connection's scope is unknown to it, so this is a guard, not a hint: a read-only connection cannot be talked into a write.
- `?max_credits=500` refuses any single credit-spending call whose up-front estimate is above five hundred credits, or that cannot be estimated, with `status: spend_limit_exceeded` and nothing charged. It is a gate on the estimate, not a meter on the charge: the estimate is what the tool would charge on its first attempt, and a tool's own retries and model fallbacks follow its usual accounting once it runs. The in-app assistant's per-turn spend tripwire does not apply over MCP, so an autonomous client without a ceiling has only the workspace's balance as a brake.

Combine them: `https://api.pamba.app/mcp?toolsets=accounts,videos&readonly=true`. A toolset name the server does not know is refused at connection time with the list of known names, and so is a `toolsets` value that names nothing, so a typo or an empty computed list shows up in your client's connection error rather than as a silently widened surface.

Every tool is annotated. `readOnlyHint` is true on reads; everything else carries `destructiveHint`, which is what clients such as Claude Code use to decide when to ask you before a call. The description of every tool that spends credits ends with its cost basis.

## What else the client receives

- **Instructions.** The initialize response carries the transport rules below, workspace selection rules (or, for API keys, the connected workspace's name, niche and website), and the assistant's full product map. Clients inject this into the model's context, which is why the model knows the order of operations without you explaining it.
- **Prompts.** Four golden-path workflows, listed as prompts (Claude Code shows them as slash commands): `first_video`, `replicate_tiktok`, `new_avatar`, `performance_review`. Each is the tool chain the assistant runs for that goal, with the waits and the spend points named. A connection only lists the prompts whose every tool is inside its scope, so a read-only or narrowed connection is not offered a workflow it cannot run.
- **Resources.** Every help-center article, including this one and the tool reference, as `pamba://help/<slug>` in markdown. The model can read them on demand, and clients that let you attach resources let you drop an article into the conversation.

## Following changes

Every public change to the MCP tools and the REST API is on the [API & MCP changelog](https://pamba.app/changelog/api), newest first (bug fixes are not listed): what was added, changed, deprecated or removed, the tools, routes and fields it touches, and, for a change that can break a working caller, what to change on your side ([breaking changes only](https://pamba.app/changelog/api?breaking=1)). To follow it without visiting the page:

- **Atom feed:** `https://pamba.app/changelog/api/feed.xml`, in any feed reader.
- **JSON:** `GET https://api.pamba.app/changelog?audience=api`, public, no key needed. `since=YYYY-MM-DD` returns the entries made or amended since that day and `breaking=true` only the breaking ones, so a scheduled job can flag a breaking change once it is listed. Poll with `since` a few days before your last read and keep each entry's id with its `updated` day: an entry is dated the day it went live, which can be a day or two before it reaches the list, and a later amendment keeps its id and sets `updated`, so treat a new `updated` as news.
- **From your client's model:** `get_recent_changes`, in the `workspace` toolset, reads the same entries (the last fourteen days when `since` is left out; `audience=api` for API and MCP changes only). Ask it what changed since a given day, or why a tool you relied on behaves differently.

## How a call behaves

- **Nothing waits for your approval on Pamba's side.** In the app, the assistant parks writes and credit spends on a confirmation card, and a turn that accumulates about eight hundred credits of estimated spend parks too. Over MCP there is no card and no per-turn tripwire: a write or credit-spending tool runs the moment the client sends it. Your client's own permission prompt is the safety net, so keep it on for anything that is not read-only, connect with `readonly=true` for exploration, and set `max_credits` on a connection an autonomous agent drives. Three server-side refusals remain: affordability, where a spend the workspace cannot cover is refused before anything runs with `status: insufficient_credits`, the credits required and the credits remaining; the plan rule, where a workspace on its free signup credits alone is refused every video generation with `status: video_generation_locked` (see Credits below); and the connection's ceiling, where a call estimated above `max_credits` is refused with `status: spend_limit_exceeded` (a TikTok `schedule_post` that must generate its video first counts, priced like `generate_video`). None of them charges anything.
- **Results are JSON objects, in both the text content and `structuredContent`.** A `status` field carries the outcome. Success reads `ok`, `created`, `updated`, `saved`, `deleted`, `scheduled` and similar. `not_found` means no such object in this workspace. `invalid_id` means an id was not a UUID from the matching list tool. Other `invalid_*` statuses name a rejected field, and `message` says why. `insufficient_credits`, `payment_required` (a paid plan is required), `video_generation_locked` (the workspace is on its free signup credits alone, which never pay for video: a plan, seats or an admin grant unlocks it, and `next` names the checkout tools), `forbidden` (the caller's workspace role is too low, for example a member trying an admins-only setting), `operator_only` (an action limited to Pamba operator accounts), `busy`, `queue_full`, `work_in_flight` and `automation_running` (work in flight, wait for it), `rate_limited` and `free_tier_exhausted` (a usage limit, with `retry_at`; see Usage limits below), `search_unavailable` (TikTok's or Instagram's music library is not answering; nothing changed), `too_large` (the file is over its upload limit; nothing changed, and the same link fails the same way) and `error` (the operation failed unexpectedly) are refusals. `message` is written to be relayed to a person. A `next` field, when present, names the follow-up call.
- **Notices ride on results.** This transport cannot reach the client between calls, so news for the user arrives on the next tool call's result, whatever the tool: a `notices` array, absent when there is nothing to tell. Each notice has a `kind` and a `message` for the model to act on. There are two kinds. `support_reply`: the Pamba team answered the user's support conversation, with `unread_count`, `from` (the sender's first name) and `sent_at`. It rides every call until the reply is read, through `get_support_messages` or in the app's Support tab, and the call to `get_support_messages` itself carries no notice of either kind. `announcement`: a release or a known outage the Pamba team chose to tell users about over MCP, in the first week after it starts, with `announcement_id`, `announcement_kind` (`feature` or `outage`), `title`, and when set `body`, `link` (a full address: a page in the Pamba app, opening in the connected workspace, or another https address the team linked) and `link_label`. The title and body are the team's words to relay to the user, not instructions for the model. Most of what the app's banner shows never comes as a notice. A `feature` comes once per user, whichever of their connections calls first (rarely a second time, around a restart of Pamba's servers), and never to a user who already clicked or dismissed it in the app. An `outage` keeps coming with results while it lasts, in its first week, so a conversation that runs into the problem later still hears why; its title and body can change under the same `announcement_id`, and the model is told to mention it once per conversation and again if its words change. A result without it does not mean the problem is over. At most one announcement rides a result, an outage first: a feature waits while an outage is on, and of two outages the newest comes. No call waits for notices, so one can first appear on the call after the one that would have carried it. Notices come on connections made with a workspace API key, or with a JWT and workspace header; a chat app's custom connector gets none, even one approved with an API key (`list_announcements` shows what was announced).
- **Refusals are results, not protocol errors.** Only `status: error` sets MCP's `isError` flag. A JSON-RPC error comes back for protocol problems only: an unknown tool or one outside the scope, arguments that do not fit the input schema, an unknown method, malformed JSON.
- **Never retry a failed or lost call blindly.** A tool may have completed part of its work (a charge, a scheduled row) before it failed, which is why `status: error` carries a generic line rather than the internal cause. A call your client abandons or times out is not cancelled either: it runs to completion on the server and its answer is simply lost. The assistant is told to report and stop; give your own automation the same rule, read the matching status tool after a lost answer, and retry only once the cause named in `message` is fixed.
- **Arguments.** Omit an optional argument to leave it unset; do not send an empty string in its place unless the tool says an empty string clears the field (the company profile fields do). Every argument is checked against the tool's input schema before anything runs, and every refusal names the argument: a name the tool does not declare is refused with the accepted names, a missing or null required argument is refused, a whole-number parameter takes a JSON integer or an integral numeric string and refuses a fraction, a boolean takes true or false, a string must be a JSON string (a parameter whose description says it carries JSON, a list of ids say, also takes the JSON list or object itself), and a parameter with listed choices refuses a value outside them. A typo therefore never runs the tool with a default in place of your value. Ids are UUIDs; take them from the matching list tool and pass them back verbatim. An avatar is addressed by its avatar project id: `list_avatars` returns both `id` and `avatar_project_id`, and almost every other tool wants the latter. Dates are `YYYY-MM-DD`. Scheduling tools take a precise ISO-8601 time in UTC (`2026-12-25T14:00:00Z`) or a date-only value that auto-picks a time in the posting window. The workspace timezone (`update_workspace_settings`) is the clock for posting windows and campaign schedules.
- **Lists are truncated, not paged.** Most list tools return the full `count` alongside a fixed-size slice and no cursor: `list_projects` forty (archived excluded unless `include_archived=true`), `list_scheduled_posts` and `list_accounts` two hundred, `list_workspaces` twenty, `list_scene_frames` one hundred, `list_video_generations` forty clips with ten takes each, or twenty-five takes when asked for one clip, `list_creative_concepts` twenty versions. Narrow with the tool's filters rather than expecting a next page. The exceptions that do page or take a limit: `list_ideas` and `search_tracked_videos` (`page`, twenty-five per page), the Discover tools (`page`, twenty per page), `browse_picker_videos` (`page`, twenty-four per page), `list_assets` (`limit` and `offset`), `get_studio_overview` (`limit` up to two hundred), `list_post_analytics` (`limit` up to one hundred, thirty by default, with `total` the full count).
- **Result size.** `get_project` returns the complete editor state and `get_content_batch` the whole batch. Read the narrower tools (`list_projects`, `get_render_job`, `list_video_generations`) when you only need a status.
- **Media URLs come in two kinds.** A `source_url` on an ingest tool (`upload_editor_media`, `set_edited_video`, `upload_scene_frame`, `upload_asset`, `import_avatar`) is fetched from Pamba's servers: public `http(s)` only, anything resolving to a private or loopback address is refused, and redirects are re-checked. Every other media field (an editor item's `src`, a post's `video_url`, `edit_image`'s `image_url`, a starting frame, `update_account_profile`'s `new_profile_picture_url`) takes a file already on Pamba storage (`https://storage.googleapis.com/pamba_videos/...`, `pamba_images/...` or `pamba_audio/...`, with no query string) and refuses a link to another host with a `message` naming the fix. Slideshow pictures and sounds, a story's `image_url` and `video_url` and a replication `source_video_url` also take a media-library link. So bring an outside file in with the ingest tool that fits and pass on the Pamba URL it returns: `upload_editor_media` for timeline media, `set_edited_video` for a project's final video (then schedule the project and leave `video_url` out), `upload_scene_frame` for a starting frame, `upload_editor_media` for a profile picture (a media-library link or another host is refused with the error code profile_picture_invalid). A media-library link from `upload_asset` is signed and expires: to put a library file on the timeline, pass its link to `upload_editor_media` and place the URL that comes back. Over REST, `POST /uploads/videos` and `POST /projects/{projectId}/upload-editor-asset` take the file itself and return its Pamba URL.
- **Conversation id.** `get_conversation_info` returns `mcp` as the conversation id because there is no chat transcript over this transport. The workspace id it returns is what support needs.

## Waiting for work

Most tools that produce media are asynchronous: the tool returns at once with a status, and the work finishes in the background over minutes. Poll the matching read tool about once a minute, and do not re-trigger the same work while a status reads pending, queued, generating, building, rendering or applying; a second call either reuses the live job or is refused.

- **Clips and videos:** `get_project` and `list_video_generations`. A failed clip generation retries itself up to three times, including an AI reword of the script after a content refusal, and falls back to the more permissive Grok Imagine model when the chosen model's safety filter still blocks it (the clip's `video_model_fallback_from` records the blocked model, and the clip's model preference then follows the fallback). A clip whose model keeps failing for a reason other than its content falls back the same way, a Grok Imagine clip to Gemini Omni, charged at no more than the chosen model's price: `video_model_fallback_from` names the chosen model and the clip's model preference stays on it. An on-camera talking take (not an `edit_clip_videos` edit) whose speech plays over a closed mouth, or that does not say its line as written (a word that carries meaning missing, added or swapped, a phrase said twice, or the line cut off before its end), is generated again on the same model, at most twice for both together, and its clip reads pending again while it waits. So a clip can sit in pending or generating for several minutes before it completes or fails for good. A project stays "generating" through the editing pass, captions, render and text hook, not just until the first clip lands.
- **Drafts and replications:** `get_replication_status` (build step, failure kind, whether a retry applies). A review-queue item with `building: true` is a draft still writing its concept, frames and script; do not generate or edit it until that clears.
- **Renders:** `get_render_job`, and `render_state` on the project (`none`, `current`, `dirty`, `rendering`, `failed`). Renders are never cancelled: an in-flight one settles and the next render supersedes stale output.
- **Avatars:** `get_avatar_wizard` for the wizard, `get_avatar_generation` and `list_avatar_generations` for redesigns, voices and scene jobs.
- **Accounts:** `list_accounts` shows a purchased seat's setup progress until the account is ready and `get_seat_billing` each seat's state; `list_account_profile_edits` tracks an avatar's profile edits, `get_account_profile_edit` one edit by its job_id, `get_account_link_status` a link and `get_account_import_status` an account import (every few seconds while it may stop on a verification code). These run on Pamba-managed phones; a profile edit takes about two minutes and a creation longer.
- **Onboarding jobs:** `get_onboarding_state` (`media_capture_status`, the presenter match, the avatar job).

A few tools block instead, for up to minutes, and return the finished result: `create_avatar`, `update_wizard_appearance`, `generate_wizard_scenes`, `redesign_avatar`, `render_project`, `update_account_profile` when it edits a TikTok profile, and `create_workspace` with a `website_url`, or with `niche_description` and `what_they_do` (it reads the site, or writes the brief from what the company does, in the call). Give them a generous client timeout.

## Credits, plans and money

- Metered operations consume credits: video generation per second at the clip model's rate (roughly sixteen credits a second on Grok Imagine, twenty-two on Gemini Omni, fifty-three on Seedance, rounded up per clip; `get_credit_pricing` is the live list), avatar photos, scenes and variations at fifteen credits an image, replication frames at about sixteen (generation plus a realism pass), clip transcription for captions at about two credits per minute of video (repeat calls served from cache), content-engine script runs, and Ideas script generation by tokens. Voices are free. `get_credit_balance` is the balance, and reads `unlimited` rather than a number for internal accounts.
- Charging is reserve-then-charge. Credits are reserved when work starts, the charge commits only on success, and a failed generation is never charged. A reservation holds for up to two hours and reduces the reported balance meanwhile, so a balance can look low while work is in flight.
- The affordability refusal is skipped for a project that already has live video work, so a repeat "generate it" reads the tool's own in-flight answer instead of a false shortfall.
- A new workspace starts with a signup grant of three hundred credits and its first avatar is free (photo, scenes, voice). A workspace on the grant alone cannot generate video: until it has a paid plan, account seats or an admin credit grant, `generate_video`, `generate_replication_draft`, a `replicate_project` without `draft_only`, a `create_video_project` with `generate_video`, and a `schedule_post` that would generate answer `video_generation_locked` (with `next` naming the checkout tools), whatever the balance. Analysis, drafts, scripts, photos, scenes and voices spend the grant as usual, and once video is unlocked the grant pays for it too. A workspace you add with `create_workspace` starts at zero.
- Top-ups come in units of one thousand credits, one to twenty units per purchase, and are available to subscribed workspaces only; `create_topup_checkout_session` returns the Stripe link. Plans: `get_credit_plans` (yearly plans grant twelve times the monthly credits up front), then `create_checkout_session` with the plan's `price_id`; for a workspace that already subscribes, the link is Stripe's confirm page, and confirming changes the plan in place: a pricier plan starts a new billing period that day with its full credits, priced at the new plan's price less what the current plan's unspent credits cost (on a subscription that has a promotion code those credits stay in the balance instead), and a cheaper plan takes effect when the current period ends. No money is refunded. Asking for the plan the workspace is already on cancels a change scheduled for the period's end and is otherwise refused. Downgrading a yearly plan is refused and goes through support. The checkout page of a new plan takes a Stripe promotion code, which the person types there; top-up and seat checkouts take none. `create_billing_portal_session` opens subscription management (payment method, invoices, billing details, cancellation; plans change only through `create_checkout_session`); a cancellation there takes effect at the period end, and access lasts until then. When a renewal is declined, seat-funded accounts keep posting and the plan's monthly credits pause while the renewal is past due or unpaid (`get_seat_billing` says `renewal_declined`; for the plan, `get_user_profile`'s `subscription_status`); `update_payment_card` returns a page for a new card, which Pamba puts on every subscription and charges the declined renewals to at once (a card changed in the billing portal does not reach subscriptions that carry their own card). A workspace's seats bill to one card unless the Pamba team split them across cards at the customer's request: then `get_seat_billing` lists `cards` (each card's seats, monthly total and renewal, all renewing on the same day), `purchase_account_seats` lists the cards a purchase can add to (pass `subscription_id`), and `update_payment_card` replaces one card only (pass the `subscription_id` of the card being replaced). Every one of these returns a URL a person completes in a browser; the purchase lands on the workspace when the payment settles, so re-read `get_credit_balance` or `get_seat_billing` afterwards.
- Purchases need the caller to be a billing admin of the workspace; other members get `forbidden`.
- A paid-plan gate returns `payment_required`: inspiration accounts, ideas and Discover details require an active subscription (a credit plan or account seats), and enabling or firing a campaign requires an active subscription on the workspace owner. Discover shows free workspaces the first rows and blurs the rest; `create_video_project` with an `idea_description` has no such gate.
- Booking a call with the Pamba team at pamba.app/book-a-call earns twenty dollars of credits, added by the team after the call.
- Usage limits are separate from credits: daily ceilings on the operations that cost Pamba compute without charging credits, set far above real use. A workspace with no plan and no account seats may start 100 renders a day (5 running at once), upload 50 videos a day and run 200 AI assists a day (`edit_video`, `reposition_bullets`, `generate_project_description`, `suggest_account_usernames`, website analysis, the title a concept save writes, the slide copy `create_slideshow` writes, the rewrite `set_draft_scene` makes, the look description a product's photos write, and the search terms written for an account's or avatar's own warming niche); a workspace with a plan or account seats may start 1,000 renders a day (25 at once), upload 500 videos and run 2,000 AI assists. Limits reset at midnight UTC. A refused call answers `status: rate_limited` with `retry_at`, or `status: free_tier_exhausted` when the free tier's shared daily budget is spent (paid workspaces are never affected). Wait for `retry_at` instead of retrying; a `start_render` refused this way has already saved the editor state you passed. Two assists degrade instead of refusing, because the request they ride on must still succeed: past the cap a concept save keeps its title, and a website analysis reports `failed` with no profile.

## What needs a browser or a phone

- **Checkout.** Every purchase is a Stripe URL the person opens.
- **Files.** Tools take URLs, never an upload body: an ingest tool's `source_url` is any public `http(s)` URL, and every other media field takes a Pamba URL (see Media URLs above). `upload_asset` stores a URL in the media library (png, jpeg, webp, gif, mp4, quicktime, webm, mp3); avatar and scene tools take image URLs. The library holds up to thirty image and video files (audio does not count) and answers `library_full` until one is deleted. Asset URLs the tools return are signed and expire within twelve hours: never store one in editor state.
- **Visual judgment.** Watching generated footage and comparing scene images is the person's call. Hand over the URLs the tools return.
- **Accounts.** TikTok and Instagram accounts are created, edited and posted from on Pamba-managed iPhones by background device jobs; a Facebook account rides the Instagram account it is linked to, posting through its cross-posts, and is never signed in on its own; `update_account_profile` edits its name, username and picture (one picture shared with that Instagram account) on that account's phone, turns on its profile's professional mode (`facebook_professional_mode`), and sets up a Facebook Page for it (`facebook_page_name` with `facebook_page_category`), after which its Instagram account's reels post to the Page, the profile is no longer edited from Pamba, and `new_profile_picture_url` and `new_bio` change the Page's picture and bio; a Page is not renamed or removed here yet, so confirm with the person first. The client never touches a phone; it schedules the job and polls. An account import is the exception that needs the person: the verification code TikTok or Instagram sends them, passed on within minutes.

## Ordering and nuance, by area

These are the rules the assistant follows and the ones people most often get wrong when driving the tools directly.

### Projects and generation

- Two ways to start a project with an avatar: `create_video_project` with `script` (spoken verbatim) or with `idea_description` (a fresh script is written on every call, so fanning one idea across avatars gives each a distinct script; never copy one generated script to several avatars). Passing both or neither is `invalid_request`; a script run that fails is `generation_failed`, and rephrasing the idea usually fixes it. `promotional=false` writes a non-promotional script with no brand weaving. A video is spoken in the language its script is written in: a verbatim `script` keeps its language, and an `idea_description` written in or naming a language gets a script written in it. Latin-alphabet languages work best. Creation costs nothing; `generate_video` is the spend.
- The project returns before its concept exists. A background job writes the script split, the default scene and the title within a minute or so; while it runs, review-queue items show `building: true` and `get_project` reports the concept as `none`. A failed build marks the project failed rather than shipping an empty draft.
- The clip split is automatic, on natural speech boundaries, with durations the project's video model allows. Non-automated creations also pick a default scene automatically from the avatar's labeled library scenes, at random among those that fit the script, so two creates from the same script can differ; when nothing fits, a random face-visible library scene stands in. Override frames only when a specific look is wanted: `set_draft_scene` moves a built script or idea draft onto another scene of the avatar's library (every clip re-deals from that scene and its pose variations at once, and its place and video prompts are rewritten for the new scene in the background, free, until anything generates; a generate that arrives first applies the rewrite before it shoots). `preview_script_clips` shows a split without saving anything.
- Authoring and generating are separate steps. Changing frames, scenes, script, concept or timeline is complete when saved; rendering or generating is a further, explicit request. The assistant is told never to queue a spend to "finish" an edit, and a script that drives the tools should not either.
- A project can exist without an avatar: `create_draft_project` with no avatar plus `set_edited_video` turns a ready-made video into a postable project. The avatar stays unassigned forever; it cannot be added later.
- Video models: `grok-imagine-1.5`, `seedance`, `gemini-omni`. The default resolves in this order: the project's explicit model, the campaign's per-member model, the avatar's `default_video_model`, the workspace routing rules (campaign videos only), then the platform default: a campaign video, a video created from a script or an idea, and a replication whose avatar speaks a line on camera currently get `grok-imagine-1.5`, from an MCP client, an API key and the Pamba app alike (clips saved by hand with `set_creative_concept` on a project that has no model yet keep `gemini-omni` unless they name one); any other video, such as a replication with no line spoken on camera, gets `gemini-omni` when an MCP client or API key creates it, while in the Pamba app an experiment currently splits it between `gemini-omni` and `grok-imagine-1.5`. `set_video_model` switches drafts and projects in bulk and re-splits the stored script under the new model's limits (skipped when every duration is already legal, because a re-split mints new clip rows). Every model authors clips of four to fifteen seconds and shares the four-second floor, and a concept save with a clip outside its model's range is refused whole.
- `get_project` uses sentinel strings: `concept` and `editor_state_json` read `none` until they exist, `final_video_duration_seconds` reads `unknown` until the file is probed, and `generated_at` stays empty until every clip has a generated video. Prefer the measured duration over summing clip durations, which are the plan. `promotional` reads `true`, `false` or `not_applicable` (verbatim scripts decide nothing).
- `update_project` changes only the fields passed: name, notes, `user_review_state` (`not_reviewed` or `approved`), `posted_state`, `user_visibility`. Archiving also cancels the project's pending posts. `list_projects` with `posted_only` means the project actually went live, which `posted_state` alone does not tell you (a cross-post downgrades a live project to `scheduled`, and cancelling a pending post resets it to `approved`).
- Approving or scheduling is refused with a conflict while the video is still generating, with one exception: clips in their final 60fps upscaling pass (the take is already playable: `active_generation_status` is null while `preview_video_url` is set) do not block; approve or schedule, and the automatic render waits for the upscaled files before anything posts.
- `generate_clips` takes optional zero-based `clip_indices`; on a clip in that upscaling pass it replaces the take: a fresh take starts, the superseded pass finishes and parks in the clip's take history (billed like any completed take, revertable with `select_clip_video`). A clip still producing its take keeps that generation. `keep_raw_voice=true` keeps the model's own audio and skips the avatar voice dub and the speech trim.
- Refusals from the clip-starting tools: `insufficient_credits`, `automation_active` (a pipeline owns the project), `queue_full` (try again in a few minutes), `work_in_flight` (a draft still building, or clips, an edit or a render from a previous generate still running). A pipeline stamp older than two hours is treated as dead, so a crashed run cannot lock a project out forever.
- `duplicate_project` clones a shell (concept and frames, no script, clips or videos); `duplicate_project_full` clones an exact independent copy, nothing re-billed. Neither carries scheduled posts, approval state or in-flight generations.

### Editing

- `edit_video` is the entry point for timeline edits: pass the intent in natural language and a dedicated editing agent, which sees the full timeline, the concept and the media library, applies validated operations and reports `applied` or `no_changes` with what it rejected and why. `edit_clip_videos` is different: it changes a clip's pixels via video-to-video on Gemini Omni whatever model made the clip, on clips that already have a video and are three to ten seconds long; for a longer clip, pass `in_point` and `out_point` to edit a window, one clip at a time, billed for the window at the Omni rate. Each edit lands as a new take, auto-selected, with the prior take still available. A failed edit leaves the working take untouched and reports on `latest_edit_status`.
- `set_editor_state` replaces the whole editor state object. Always read it from `get_project`, modify, and write it back; a partial object deletes what it omits. When changing one field of an existing item, copy the item verbatim: an item with the same id inherits its earlier placement and style for fields you leave null, but explicit values always win.
- Editor item shapes: text items place with `x`, `y`, `w` normalized zero to one (`x` 0.5 centered, `y` the vertical center: a top hook sits near 0.12 to 0.2, a bottom one near 0.8), and `style.font_size` is pixels on a 1080 by 1920 reference (default sixty-four, readable minimum twenty-four). Always set `x` and `y` or the text renders center-frame. Text wraps only at its own line breaks unless `text_wrap` is true, which makes `w` the box width. Prefer `style_preset` ids from `list_text_style_presets` over hand-built styles. Media items (and audio items) need a Pamba image, video or audio URL: bring an outside file in with `upload_editor_media` and place the URL it returns (a signed library asset URL is refused). They take `w` as a width fraction (1.0 fullscreen, about 0.6 for a picture-in-picture, about 0.4 for a logo flash); video items also take `speed` 0.5 to 2 and `volume`, which you set to zero so a demo clip never talks over the presenter.
- Clip ids are re-minted on every concept save. An id held across a save answers "Clips not found in the selected concept"; re-read the project. Changing a clip's starting frame also drops that clip's generated video, which comes back only by reverting to the prior concept version (`list_creative_concepts`, then `revert_creative_concept`, which restores script, clips and editor state together and is refused while automation runs).
- `select_clip_video` needs a completed take of the same project; re-render afterwards to bake the change in. A main-track entry with `use_original_voice: true` keeps the clip's pre-dub audio in renders, and the flag is dropped when the clip re-points to a new generation.
- Captions come from `transcribe_clips`, which is English only, so the on-screen captions timed from the speech suit English videos. Word timings arrive in spoken order and must not be re-sorted.
- Renders are automatic when saved edits are newer than the last render: approving, scheduling and leaving the editor re-render, scheduled posts wait briefly for an in-flight render rather than posting stale video, and a sweeper renders recently active projects once they have been quiet a few minutes. `render_state: failed` means the last attempt for the saved edits failed and only the next edit or commit point retries it. A video supplied directly is recorded as the current render and is never re-rendered over: `set_edited_video` copies whatever file it is given, and a `video_url` on `update_scheduled_post`, `schedule_instagram_post` or the REST schedule route counts only when it is a file from outside the project's own outputs. A url the project itself produced (its `edited_video_url` or `stitched_video_url`, an earlier render, a file it was given before) is ignored on those post routes, and the pending post keeps following the project's latest render.
- `set_clip_voice_effects` adjusts the acoustic environment baked into generated clips' audio: free, seconds per clip, frames untouched.

### Replication and drafts

- `replicate_project` takes exactly one source: a `tiktok_url`, an `instagram_url` (a reel; `inspect_instagram_reel` reads a pasted link first), a `source_video_url` from the workspace, or a `source_project_id` of one of the workspace's own projects (its rendered video is the reference, so it needs a finished video). `draft_only=true` builds a reviewable draft and stops before clip generation, so video credits are spent only when the draft is generated. `max_duration_seconds` is a target, not a cap: it shortens or lengthens the replica, and every replica is capped at one hundred sixty seconds whichever model generates it. `variation_instructions` cap at four thousand characters. In a workspace whose replications draw motion graphics (a trial in a few workspaces), the graphics follow how the reference draws and animates its own; ask for simpler or more impressive ones in `variation_instructions`. A workspace runs ten replication jobs at once; requests past that queue automatically.
- For `source_video_url`, use a Pamba-hosted video or a video asset owned by the selected workspace's media library. Upload an external source to that library first; arbitrary remote URLs and local files are refused before replication starts.
- Replicating from a TikTok URL also captures the source into the ideas library, deduplicated on the video URL; a replicated Instagram reel is not captured. Replication plans media from the workspace asset library on its own: an app demo the original cut to becomes a media scene, partial cards and logo flashes become timed overlays on a Media track. A non-promotional replica does not use the brand media library.
- A hub video judged hard to replicate returns `low_feasibility_warning` until the call repeats with `acknowledge_low_feasibility=true`. A video that needs the brand's own digital files returns `missing_brand_assets` naming what to upload (`upload_asset`) rather than starting; the same acknowledgement proceeds anyway and the replica drops those beats. Physical products are never a requirement.
- `get_replication_status` reports `build_step` (`source`, `analyze`, `concept`, `scenes`, `frames`, `assemble`, `done`) and, on failure, `failure_kind` (`transient`, `timeout`, `safety`, `quality`, `content`, `unknown`), `retriable` and a `next` hint. `retry_draft_build` resumes from the last completed checkpoint, so finished stages are never re-paid; prefer it for transient and timeout failures and `request_draft_edits` for safety or content failures, because a blocked look keeps getting blocked. A failed request-edits revision restores the draft intact and reports `failed_revision` with the preserved feedback.
- An ungenerated replication draft's editor is read-only: direct `set_editor_state` is refused. Its edit surface is the draft itself, through the edit ladder: words only, on one scene, `edit_scene_script` (free); the whole read, `edit_draft_script`; one scene should look or play differently, `edit_scene` (image credits); the whole video's look, `regenerate_look_canon`; restructure, duration, add or remove scenes, `request_draft_edits`, which `edit_scene` refuses and which cannot serve script drafts. Scene edits of different scenes run in parallel; same-scene and whole-script edits serialize. Every completed `edit_scene` is undoable with `revert_scene_edit`, per scene, free.
- A draft can complete with some scene frames failed; `regenerate_scene_frame` retries them, and `generate_replication_draft` is gated until every scene has a frame.
- `generate_replication_draft` approves the reviewed draft into clip generation plus the final editing pass and render, honoring editor changes present on the draft as deliberate overrides. `generate_video` on a draft instead runs the standard automation pipeline and does not carry draft edits as overrides.

### Scheduling and posting

- `schedule_post` posts a project's video to TikTok (`tiktok_username`), to the avatar's Instagram (`also_post_to_instagram`), or both in one atomic schedule: if either platform has no slot (device busy, no slot left that day), nothing is scheduled and the whole request is rejected. `schedule_instagram_post` schedules a standalone Reel. Facebook gets an Instagram video reel only through Instagram's share: a reel goes to the account's linked Facebook account while the account's Cross-post reels to Facebook switch is on (`set_account_facebook_cross_posting`, `update_avatar_settings`'s `facebook_cross_post_enabled` for an avatar's Instagram accounts, on for a new account; `list_posting_accounts` names the accounts whose switch is on in `instagram_cross_posting_to_facebook`) and that Facebook account is posting, checked again when the reel posts. Leave `share_to_facebook` out to follow the switch, pass `false` to keep one reel off Facebook; `true` is refused when it cannot happen (`facebook_cross_post_off` while the switch is off, `no_linked_facebook_account`, `facebook_account_paused`). A post's `share_to_facebook` says what it will do as of now and `share_to_facebook_choice` what it asked (empty: follows the switch). An Instagram Reel can go out as a trial (`schedule_post`'s `instagram_trial_reel`, `trial_reel` on `schedule_instagram_post` and `update_scheduled_post`): `release_to_followers_once_it_performs_well` lets Instagram share it to everyone if it performs well in its first 72 hours, `release_to_followers_never` keeps it a trial that is never shared to the account's followers; leave it out for a regular reel, and `update_scheduled_post`'s `remove_trial_reel` takes a trial off. Instagram shows a trial to non-followers first and keeps it off the profile; it never goes to Facebook (refused with `share_to_facebook` true), Instagram offers it only to professional accounts with at least 200 followers (an account under 200 is refused with `instagram_trial_reel_ineligible`: its last read follower count decides, or, for an account Pamba has not read yet, its profile read when the post is scheduled; `list_posting_accounts` and `get_project` name the accounts known to be under it in `instagram_below_trial_floor`; a personal account is refused the same way, and they name the ones Pamba knows in `instagram_personal`: switch one to creator with `update_account_profile`'s `instagram_account_type` and the trial can be scheduled as soon as the switch is queued), and a slideshow's carousel has none. `update_scheduled_post`, `cancel_scheduled_post` and `retry_scheduled_post` manage the queue; `list_scheduled_posts` and `get_scheduled_post` read it. A `completed` post is the phone's word that it posted, and its `verification_status` says whether Pamba then found it on the account's public profile: `pending` while the checks run (20 minutes after posting, then an hour, another hour and a day later), `verified` once it matched (its link in `posted_tiktok_video_url` or `posted_instagram_video_url`; an Instagram slideshow post (a carousel, or a reel built from its slides), an Instagram trial reel (linked once a reel with its own caption appears on the account), and an Instagram reel posted before 2026-10-03, read `verified` on the phone's word alone until the link is known), `video_not_found` when no new post appeared, `frame_mismatch` when a new post appeared that is not this one, `error` when the check could not finish; `verification_message` says it in one sentence. The check runs once, so `verified` says the post was found, not that it is still up. The app shows these as Posted, Verified (for an Instagram post, once its link is known), Not visible and Not matched. A post deleted through Pamba (`delete_account_posts`) before the check found it, on TikTok or Instagram, is not checked further once the deletion is confirmed: its `verification_status` turns empty and the app shows Posted. Pamba knows it by the video id the phone read as it posted, or, for a post without one (an Instagram reel, a TikTok photo post), by the deleted post of its kind that went up while it was being posted, with its caption; such a reel keeps the deleted reel's link in `posted_instagram_video_url`, which is no longer live. A post without a caption, or one whose deletion is still being confirmed at the last check, is checked as usual and can read `video_not_found` or `error`. A failed post whose retry already failed again, in a way that alerted Pamba's engineers, reads `being_fixed` true: it is theirs to fix and retry, usually within a few hours, and `retry_scheduled_post` refuses it with status `being_fixed` (the REST route answers 409 `post_being_fixed`). A hold nobody works ends a day after the failure, and the retry is yours again. A failure you remedy yourself (another Instagram song or sound, the slideshow's missing images, the TikTok Shop link, approving the project, reconnecting a signed-out account you connected: `account_needs_reconnect`, new audio for a video TikTok would mute for copyrighted music: `copyrighted_sound_muted`, where a TikTok sound added on top keeps the video's own audio, another TikTok sound for a post on a TikTok Business account whose sound isn't cleared for commercial use: `sound_not_licensed`, another TikTok sound for a post whose sound TikTok says isn't available: `sound_unavailable`, a reel Instagram offered no trial: `ig_trial_reel_unavailable`, fixed by switching the account to creator with `update_account_profile` or removing the trial with `update_scheduled_post`'s `remove_trial_reel`, a caption Instagram flagged after Share as similar to others that have been reported: `ig_caption_flagged`, nothing posted, fixed by a new caption with `update_scheduled_post`) stays yours to retry. A trial reel whose outcome was lost reads `being_fixed` true from its first failure, and that hold does not end after a day: it may be live and no lookup can prove a trial absent, so Pamba's engineers check it on the phone and settle it, and `retry_scheduled_post` refuses it with status `being_fixed`. A post is never retried, by anyone, while another post of the same video or slideshow to the same account is scheduled, posting or already posted, so nothing posts twice. And one video or slideshow goes to one account per platform: `schedule_post`, `schedule_instagram_post` and `update_scheduled_post` refuse with status `duplicate_content`, and `schedule_slideshow_post` with status `rejected` and `code` `duplicate_content` (over REST, `error_code` `duplicate_content`, and `code` on the slideshow schedule route), a post whose content another account of the workspace has scheduled, is posting or posted on the same platform (a draft-only post, `only_create_draft`, on TikTok or Instagram, never counts: it never goes public): the same project or slideshow, or a file with the same bytes (the same upload twice; a carousel all of whose slides another account's carousel holds, or that holds all of another's). TikTok and Instagram flag the same content on a second account. The same video on TikTok from one account and on Instagram from another is fine, and an upload re-cut, re-encoded or with other words on it is another file.
- **Pick a time the phone has free.** Each account posts from one Pamba iPhone, and a phone runs one post or task at a time for every account on it (an avatar's TikTok and Instagram accounts usually share one). An exact TikTok time that overlaps another post, a task running on the phone or booked on it to set up an account, or a maintenance window is refused as `schedule_conflict` with the next free time in `message`; an exact Instagram time there is moved to the next free slot that day (read the returned `scheduled_at`) and is refused only when the day has no room left. The refusal carries a `next` field pointing at `get_account_schedule`: `schedule_post` and `schedule_instagram_post` answer it as their status, `schedule_slideshow_post` as its `code`, and `update_scheduled_post` as `update_failed` with `code` `schedule_conflict`. That tool (`platform` `tiktok` or `instagram`, `username`, optional `from` and `to` in UTC, seven days by default and fourteen at most) returns `openings`, the stretches in which a post picked at an exact time is kept as picked (a schedule with automate, or while its video is still being finished, waits at least 30 minutes from the request instead) (whole minutes, both ends included, none before `earliest_post_at`, which moves with the clock: a pick must still be that far ahead when it is sent), `busy`, what holds the phone (`post` with this workspace's `post_id`, `post_platform` and `post_username`, `task`, or `maintenance`), and `post_minutes`, how long one post holds the phone. Read it before scheduling at exact times. A new account being set up on a phone goes first: its setup is booked on the phone from the start, listed in `busy` as a `task` at the time it is expected to reach the phone, and a post that comes due while Pamba creates an account on its phone waits for the setup and goes out soon after it, its `scheduled_at` moved to the new time, and the wait costs none of its retries. Each post you schedule holds its phone for `post_minutes` from its time, so space exact picks on one phone at least that far apart. When a TikTok and an Instagram account share a phone, the Instagram post takes the first free time after the TikTok one that day (Pacific time; right after it when the phone is free), and a day with no room left for it refuses both, so leave `post_minutes` free after a TikTok time that cross-posts; a Facebook share rides the Instagram post. A post you move with `update_scheduled_post` is not blocked by itself or its pending cross-posts; the cross-posts then follow it to the first free time after the new time that day, and one with no room left keeps its old time, so leave `post_minutes` free after the new time for each cross-post on the same phone. A new caption on the TikTok post likewise carries to its Instagram cross-posts in the same status (pending with pending, failed with failed) that still had the old caption; one whose caption no longer matched keeps it, and editing a cross-post never changes the TikTok post. A post `schedule_post` booked away from the exact time asked for carries `moved_from` (that time) and `move_reasons`, a list of `too_soon`, `video_to_make`, `video_finishing`, `accounts_setting_up` and `phone_busy` in the order they applied, with a note: tell the user the new time and why. A date-only `scheduled_at` finds a free slot by itself. The openings are a read, not a booking: another schedule can take the time first, and on rare occasions the fleet-wide cap on posts at one time refuses a pick inside one, again with the next free time. Over REST it is `GET /devices/accounts/schedule`.
- **How often an account should post.** An account Pamba created less than 10 days ago does best posting once a day, and any account at most 2 or 3 times a day: past that each post brings diminishing returns and makes TikTok or Instagram likelier to flag the account. Pamba warns and never refuses. A post that makes a new account's 2nd post in a day (the workspace's day, counting its pending, posting and posted posts) or any account's 4th is booked all the same and carries `posting_warning` in the result of `schedule_post`, `schedule_instagram_post`, `schedule_slideshow_post`, and `update_scheduled_post` when the edit moves the post: tell the user the warning and offer to move the post to another day. `list_accounts` sets `new_account_until_utc` on each account on the workspace's phones that Pamba created less than 10 days ago, so spread several posts on one account across days before scheduling them. Over REST the schedule and edit responses carry `posting_warning` as `{ code, message, day, posts_that_day, posts_per_day }`, `code` being `new_account_posts_per_day` or `posts_per_day`.
- A project with an avatar may only post to a TikTok account linked to that avatar. A project with no avatar can post to any account on the workspace's fleet, linked or unassigned; posting to an avatar's account attributes the analytics to that avatar, posting to an unassigned account attributes them to none. `get_project`'s `posting_accounts` (and `get_slideshow`'s, for a slideshow) lists the accounts a schedule may name, the choices the app's schedule dialog offers, and `list_posting_accounts` lists every avatar's and the no-avatar pool at once: read one before `schedule_post`. A listed TikTok account that is not signed in on one of the workspace's phones, or one with an open suspension, is still refused.
- **Posting a video you already have** (nothing generated, rendered or credit-metered): `create_draft_project` makes an empty project with no avatar, so it can post to any account on the fleet; `set_edited_video` attaches the file from a publicly reachable video url and records it as the project's completed render; then `schedule_post` or `schedule_instagram_post` posts it. There is no upload body over MCP: a file on the client's machine is staged with the REST upload (`POST /uploads/videos`, multipart, the same API key) and its returned url passed to `set_edited_video`. To attribute an uploaded video's analytics to an avatar, create the project in the app's Upload button or with the REST create call and its avatar field, then attach and schedule from here; the avatar choice is permanent. `set_edited_video` is refused while automation runs on the project or a replication draft is locked, and a scheduled post for the project picks up the new video.
- What `schedule_post` posts depends on the project: a project with a concept whose clips have no video yet is generated and rendered first (the result says `mode: generate_then_post`, credits are spent); a project whose video was supplied (`set_edited_video`, the app's Upload button) posts that video as is (`mode: post_project_video`, nothing generated); a project with neither a video nor a concept is refused as `nothing_to_post`. The REST `automate` flag follows the same rule: it is 400 on a project with no concept to generate from. `generate_video` follows it too: a project whose video was supplied, or one with no concept at all, is refused as `nothing_to_generate` instead of starting a pipeline that has nothing to build.
- A post goes out only if its project is approved at its slot. `schedule_post` says where that stands in its result's `approval` field: `approved_by_schedule` when it posts a video that already exists (the schedule approves the project, nothing more is owed), and `required_before_slot` in `generate_then_post` mode, where the schedule does not approve a video that does not exist yet and someone must approve the rendered video before the slot. `update_project` with `user_review_state=approved` approves, with or without a schedule; approving is refused while the video is still generating. A post that failed at the gate is recovered by approving the project and then calling `retry_scheduled_post`. Automated pipelines (campaigns, content pushes) generate, render and schedule but never approve on their own, so a scheduled post whose project nobody approved fails at its slot with the message "Project not approved at scheduled time". That is the review step working, not an outage.
- Captions: `generate_project_description` writes a hook line plus exactly five hashtags from the script and company profile, but only returns the text; pass it as the caption yourself. `schedule_post`'s `caption` goes out on every platform it schedules; pass `instagram_caption` as well to give the Instagram Reel its own (the Facebook share carries it too) while TikTok keeps `caption`. It needs an Instagram leg, and each caption is checked against its own platform's rules. A TikTok caption with one of the commercial-disclosure hashtags `#ad`, `#ads`, `#sponsored`, `#partnership` or `#paidpartnership` is posted with TikTok's commercial content disclosure on, as Branded content ("You are promoting another brand or third-party"), so TikTok labels it "Paid partnership" and Pamba cannot take the label off once it is live. TikTok publishes no post with such a hashtag without the disclosure. `generate_project_description` never writes one: whether a post is an ad is the user's call. An `@handle` in a caption, a video's or a slideshow's, posts as a linked mention on TikTok and Instagram alike: on TikTok the phone picks each account from TikTok's mention list as it types, so a TikTok caption holds at most 5 mentions, uses `@` only to start one (no email addresses, no handle run into other letters such as `@my-brand`) and never names its own posting account, and a handle TikTok's list does not offer fails the post with `mention_not_offered` before anything is posted (fix the handle with `update_scheduled_post`, then `retry_scheduled_post`). An Instagram caption, a reel's or a carousel's, holds at most 2,200 characters and 5 hashtags: Instagram keeps no more than 5 hashtags on a post, so a longer list is refused rather than posted with the rest stripped to plain words.
- When the avatar has no account on a platform yet, `schedule_post` with `await_new_account=true` (TikTok) or `await_new_instagram_account=true`, or `schedule_instagram_post` with `await_new_account=true`, persists the post as waiting; `purchase_account_seats` with `link_avatar_project_id` then buys the account, and once it is created the waiting post books a real slot and goes out with no further action, independently per platform.
- **TikTok Shop.** A TikTok account enrolled in TikTok Shop can link one product of its shop to each post, chosen per post (there is no default product). `get_tiktok_shop` reads the shop as the account's phone last saw it (`linked`, the showcase, the catalog's products with their ids and `showcase_state`); `sync_tiktok_shop` reads it again on the phone, which takes a few minutes. The account's daily phone visit also looks for the Shop tab and reads the shop by itself when the tab appears or goes, so an account TikTok enrolls shows up without a sync. Pass `tiktok_product_id` (and optionally `tiktok_product_link_name`, at most 30 letters, numbers, spaces, apostrophes or hyphens) to `schedule_post` or `update_scheduled_post`; `remove_tiktok_product` takes it off a pending or failed post, and a post moved to another account drops a product it does not restate. Only a video post links a product; a slideshow cannot. A post links from the showcase only, so a product not in it is queued for adding the moment the post is scheduled (`add_tiktok_showcase_products` does the same on its own), and the post goes out only after the phone can have added it: an exact time before that is refused with status `tiktok_product_not_in_showcase_yet` and `earliest_at_utc`, and a date-only time is given a slot after it. The phone reads a product's title off the screen, so a long one reads cut short with … at its end; `rename_tiktok_shop_product` gives a product its `display_title`, the name to show for it (the phone keeps finding it by its title, and a read of the shop never changes the name). The phone reads each of the showcase and the catalog up to about 80 products; a list it could not read whole (longer than that, or with a product the phone saw but could not read) reads `showcase_complete` or `catalog_complete` false, and the lists then hold part of the shop. The phone adds the link before its Post tap, and a post whose link cannot be added fails rather than go out without it. TikTok caps a new shop's shoppable videos (`shoppable_videos_left`, as the latest post that linked a product left it).
- **AI label.** `ai_label: true` on `schedule_post` labels the video as AI-generated on every platform that call schedules (TikTok's "AI-generated content" label, and Instagram's "Add AI label" switch, which shows on the post as "AI info"); `schedule_instagram_post` takes it for the reel, and `update_scheduled_post` turns it on or off for one pending or failed post. TikTok and Instagram video posts only: a slideshow post and a Facebook post refuse it. The phone sets the label before its Post or Share tap, and a post it cannot label fails and is retried rather than go out unlabeled. Pamba cannot take the label off once the post is live, so set it only when the user asks.
- **First-frame cover.** Left alone, TikTok and Instagram pick a video's cover from a frame a few seconds in. `first_frame_cover: true` on `schedule_post` makes the video's first frame, where its burned-in hook is, the cover on every platform that call schedules; `schedule_instagram_post` takes it for the reel, and `update_scheduled_post` sets it (true) or leaves the cover to the platform again (false) for one pending or failed post. TikTok and Instagram video posts only: a slideshow post and a Facebook post refuse it. The phone sets the cover in the app's Edit cover before its Post or Share tap, and a post it cannot set it on fails and is retried rather than go out with another cover.
- If a schedule is rejected, read the reason and offer the valid accounts or another time; do not retry blind.

### Slideshows

- A slideshow is a TikTok photo post or an Instagram carousel (or, with `as_reel`, one reel Instagram builds from the slides): up to twenty pictures with words burned in, and one sound per platform under the whole post: a TikTok sound for the photo post, an Instagram track for the Instagram post. It is made from pictures the workspace already holds — nothing about a slideshow spends credits, and there is no slide image generation. Get the pictures in first: `upload_asset` with a public url (or `list_assets` for ones already there), then `create_slideshow` with `slide_images` (one Pamba-hosted url per slide, in order) and `lines` (`write_with_ai=false` keeps them verbatim; no lines gives picture-only slides). Both are strings: a JSON array serialised as a string, or one entry per line; the JSON array itself is taken too. A slide with a picture but no line has no text element.
- The tools are in the `content` toolset (`list_slideshows`, `get_slideshow`, `create_slideshow`, `update_slide`, `add_slide`, `remove_slide`, `reorder_slides`, `add_slide_image`, `update_slide_image`, `remove_slide_image`, `suggest_slide_lines`, `search_tiktok_sounds`, `set_slideshow_sound`, `search_instagram_audio`, `set_slideshow_instagram_sound`, `schedule_slideshow_post`, `get_slideshow_scheduled_post`, `duplicate_slideshow`, `export_slideshow`, `update_slideshow`). Every edit re-burns the slide; `get_slideshow` reports each slide's `status` (`ready` once burned) and its `image_url`.
- Sound: `search_tiktok_sounds` with no `query` returns TikTok's Hot chart (`source` `hot`, in chart order, the editor's default listing); with a `query` it pages TikTok's own library (`cursor` from the previous page's `next_cursor`), and `set_slideshow_sound` picks one for the whole post by `tiktok_music_id` (the search result's `music_id`); the search is a plain read, not one of the capped AI assists. Searches that reach TikTok run at most 5 a second, 30 a minute and 1,000 a day per workspace (a repeat the day's cache answers is free); past that the tool answers `rate_limited` with `retry_at` (midnight UTC once the day's are used), and the REST route a 429 with a `Retry-After` header. Every result carries `commerce_right`: whether TikTok clears the sound for commercial use, which a Business account needs to post with it (its sound page shows no Use sound otherwise), so prefer a cleared sound for such an account. When TikTok's sound search or sound page is not answering, `search_tiktok_sounds`, `set_slideshow_sound` and `set_video_tiktok_sound` answer `search_unavailable` (nothing changed; try again in a minute) and the REST routes a 503.
- Instagram sound: the Instagram post's sound is picked apart from the TikTok one, because the two platforms' libraries are different. `search_instagram_audio` with no `query` returns Instagram's Trending chart (`source` `top`, read from a US account's picker); with a `query` it returns Instagram's own results for the words, ten at most with no further page, and the same per-workspace rate and daily limit as the TikTok search. `set_slideshow_instagram_sound` picks one by `instagram_audio_id` (the result's `audio_id`; `none` clears it). The track starts where Instagram's picker starts it (`highlight_start_seconds`), and the phone picks it in Instagram's own music picker when the Instagram post goes out. One it cannot find there goes out as the same song by the same artist at another length, else as a song Instagram picks, and `get_scheduled_post` and `list_scheduled_posts` name it in `audio_fallback`. The searches run on accounts outside the US, which list some songs US Instagram lacks, so a song a Pamba phone did not find, posting from an account Pamba knows is not a Business account (a Business account's picker lists only commercial music), is recorded and left out of every later search and of the chart. A pending or failed post still using such a song reads `instagram_song_unavailable` true on `get_scheduled_post` and `list_scheduled_posts`: set another song before it goes out to choose it yourself. A pick the chart or the day's searches did not list is looked up on Instagram's audio page, and that lookup counts against the same per-workspace rate as a search (not the daily limit, which counts searches only). No provider says which songs an Instagram Business account may use, so the whole library is listed. When Instagram's music search or audio page is not answering, the tool answers `search_unavailable` and the REST route a 503.
- Posting: `schedule_slideshow_post` schedules the slides on a Pamba-managed iPhone and approves the slideshow; it is refused while any slide still has no picture. A TikTok photo post takes a single slide, and so does an Instagram post (`platform` instagram or both), where one slide goes out as a single photo and two or more as a carousel; a reel built from the slides (`as_reel`) needs at least 2. With `platform` tiktok (the default: `tiktok_username`, `scheduled_at`, `caption`; the caption is the post's description, capped at TikTok's 4,000 characters with the same hashtag and mention rules as a video, and the post has no separate title) it is a public photo post carrying the slideshow's sound; with `platform` instagram (`instagram_username`, omitted when the avatar has one Instagram account; `scheduled_at`; `caption`, at most 2,200 characters and 5 hashtags, its `@handle`s linked by Instagram) it is a public carousel carrying the slideshow's Instagram sound, each slide fitted whole into Instagram's square or 3:4 feed frame (square when the slides are square or wider), and it is refused until the slideshow has one (`set_slideshow_instagram_sound`): without a track Instagram does not treat the carousel as a reel and shows it only to followers, and the track cannot be cleared while an Instagram post waits; `as_reel=true` posts it instead as one reel Instagram builds from the slides, each 4 seconds with the same track under it (which the reel needs too), in the full 9:16 frame with the first slide as its cover (refused with `platform` tiktok; with `platform` both the TikTok photo post is unchanged); with `platform` both (`tiktok_username`, and `instagram_username` as above) the two are booked together in one plan, both or neither, under one `caption` that must pass both platforms' rules, and the result's `posts` lists each. A slideshow posts once per platform. `get_slideshow_scheduled_post` follows them (`posts` holds one per platform: `pending`, `in_progress`, `completed`, `failed`), and `cancel_scheduled_post` with a post's id cancels it while it is still pending. A TikTok photo post is verified against the account's profile and gets analytics like a video. An Instagram carousel is verified on the phone's word (the phone reads it as the profile's newest post), and the account's hourly analytics refresh then links it to its live post (`posted_url`, a `/p/` link) and tracks its likes and comments; Instagram publishes no views for a carousel. A slideshow's reel is verified the same way, then linked among the account's reels (a `/reel/` link) with its public views; each Instagram post in `posts` carries `as_reel`.

### Avatars and voices

- The wizard, in order: `create_avatar` (blocks until the candidate scenes exist and charges seventy-five credits: one appearance plus four scenes), then any of `discard_wizard_scene`, `generate_wizard_scenes` (fifteen credits a scene) and `update_wizard_appearance` (regenerates the look) before `accept_avatar_appearance` locks the appearance, then `select_avatar_voice`, which finishes the wizard. `get_avatar_wizard` polls and already lists the free `voice_options`; `reroll_preset_voices` swaps in three other free picks (`direction` accepts only `deeper` or `higher`). Completion auto-starts pose and angle variations for every kept scene, free on the workspace's first avatar and charged per image after, so do not also call `generate_scene_variations` on a fresh avatar. The first kept scene becomes the profile image.
- Voices are curated human recordings matched to the avatar's gender and age, all free (called "preset" in tool names for historical reasons). Each comes labeled by position, `Voice 1`, `Voice 2` and `Voice 3`, the only name to give it. `get_preset_voices` then `apply_preset_voice` changes an existing avatar's voice; browsing, rerolling and applying are free. There are no paid voices: an avatar that already has a premium voice keeps it.
- `update_avatar_settings` replaces whole sets: `tiktok_usernames`, `tags` and `warming_search_terms` are overwritten with what you pass (an empty list clears, and warming then falls back to niche terms), and the TikTok accounts must already exist on a workspace device. One exception keeps a new account from being dropped: a TikTok account `get_avatar` lists with `name_pending` (its picked username is still being applied) stays linked while its rename runs and for an hour after it lands, whatever set you pass, and the set names it by its picked username; `unlink_account` removes it. `default_video_model` takes `default` to clear back to the workspace routing rules.
- Scene library: every scene and library frame carries vision-derived `content_labels` from a fixed vocabulary plus a `scene_description`; match those against a clip's needs rather than picking by name. `generate_scene_frames` (one to ten, default three) lands frames inactive; activating one with `update_scene_frame` auto-generates six pose variations (ninety credits) when it has none, and `upload_scene_frame` is stored active and does the same. `delete_scene_frames` also deletes a scene's variations, and the profile frame cannot be deleted; set another first. Replication-generated scenes stay out of the avatar's active library on purpose.
- `edit_image` edits any Pamba-hosted image with a text instruction (fifteen credits) and returns a new URL; on a registered frame the result is itself usable as a starting frame. `save_scene_variation` keeps that result as a variation (original untouched), `replace_scene_image` points the frame at it instead, losing the original. `promote_scene_variation` swaps a variation with the scene's reference, so promoting it back undoes the swap.
- Camera styles are `selfie`, `tripod`, `tripod_full_body_visible` and `podcast` on the avatar tools; `generate_starting_frames` spells the full-body style `tripod_full_body`, and the other spelling falls through to selfie there.
- `import_avatar` creates an avatar from a photo URL with `gender` `MALE` or `FEMALE` and a free voice; `redesign_avatar` applies free-form appearance or voice changes and blocks until they land. `track_external_tiktok_account` attaches an account you post to from your own phone, for analytics only: posting and warming stay unavailable for it. A free workspace (signup credits only) can connect up to five such accounts; the sixth answers `connected_account_limit`, and any plan, account seat or credit purchase lifts the limit.
- Operator-only tools are listed only to Pamba staff. A customer workspace or a connected chat app never sees them, and calling one by name is an unknown tool: the reference-frame tools (`create_reference_frame`, `update_reference_frame`, `delete_reference_frame`), `create_social_account`, `get_account_creation_status`, `resume_account_creation`, `generate_image`, the writer-engine controls (`run_content_engine`, `push_content_batch`, `apply_script_revision`, `set_script_starting_frame`, `decide_learning_item`, `set_autonomy_dial`, `set_batch_share_page`, `send_writer_chat_message`), and the engine's spend bookkeeping (`log_llm_usage`, and `get_llm_usage_summary`, which reports the model spend at Pamba's cost). `control_stage_run` is listed to everyone, but cancelling a run, and retrying some runs, answers `operator_only` for a customer workspace.

### Accounts, seats and devices

- `list_accounts` is the inventory of every TikTok, Instagram and Facebook account on the workspace's fleet, including accounts assigned to no avatar (their avatar fields are null), and, listed last, its old accounts: an archived account it held (one of its seats named it, or it posted from it) stays on the list, frozen and with `on_device` false, after it leaves its phones, as an account a recreation replaced does. Status is `posting`, `track_only` or `archived`; health flags include `BANNED`, `SHADOWBAN_RISK` (after the account's first three weeks of tracking), `NOT_RECOMMENDED` and `SUSPENDED`.
- `set_account_order` saves the order the workspace's Accounts page lists accounts in, for everyone in the workspace, and `list_accounts` reports each account's `position` in it. A save replaces the whole order, so moving one account means sending every account in its new place; accounts left out, and accounts added later, follow in the default order (open issues first, then the most viewed, a recreated account counting its seat's views so it keeps the row of the one it replaced), and under a saved order archived accounts are listed last. An empty list goes back to that default.
- Managed account seats cost one hundred dollars a month each, and every two of a platform get a phone of their own for that platform. A workspace whose owner signed up before the TikTok plus Instagram pair discount ended for new customers keeps it (`purchase_account_seats` with no seat counts returns `bundle_eligible` true): every TikTok plus Instagram pair across the subscription prices its second account at fifty dollars, re-priced when seats are added or cancelled. Every other workspace pays one hundred dollars for every seat, and its `unpaired_seats` is empty. From Monday, November 16, 2026 at midnight Pacific no new seat pairs: `bundle_eligible`, which says whether the seats bought now pair, is false and `unpaired_seats` is empty unless Pamba agreed a different price with you, so every seat bought from then on is one hundred dollars a month. The price of the seats you have is on `get_seat_billing` and each account in the inventory, never `bundle_eligible`. Seats bought before then keep their pair price for as long as both seats of the pair bill, an account recreated on its seat or a seat the Pamba team moved to another card included; a seat cancelled and bought again is a new seat at one hundred dollars. Until then, for a workspace with the discount, a paid purchase also pairs with seats already on the subscription it raises, the newest one that is active and not set to cancel: `purchase_account_seats` called with no seat counts returns `unpaired_seats`, per platform the seats on that subscription with no partner yet, and the next paid purchase pairs up to that many seats of the other platform with them. One Instagram seat next to one unpaired TikTok adds fifty dollars a month, not one hundred; two Instagram seats next to it add one hundred fifty. Only what `unpaired_seats` lists pairs this way: an account with no seat, a seat on a free-seat grant (`free_seats_remaining`) or on another subscription, and a seat cancelled with `cancel_account_seat` (even while it posts until its period ends) pair with nothing, and when no subscription can be raised, the purchase starts a new one where only its own seats pair. Any quantity up to a thousand accounts per checkout can be bought; the seats the phones cannot back yet stay being set up until the team adds phones. While Pamba's phones are short of room for new accounts, `purchase_account_seats` returns `high_demand` true and every checkout it creates carries `high_demand_notice`, whatever its seats: new accounts may take up to 5 days to be created (for purpose `connect`, the accounts may wait up to 5 days for a phone to sign in on), and the user hears so before paying. Facebook is an add-on to an Instagram account: a flat one hundred dollars a month on top of its Instagram seat that never pairs (Instagram with Facebook is two hundred dollars a month, a TikTok plus Instagram pair with Facebook three hundred, or two hundred fifty with the pair discount). It is rolling out: `purchase_account_seats` called with no seat counts returns `facebook_addon_offer`, none or with_new_accounts. `facebook_for_instagram` adds it to Instagram accounts the workspace already has: only one Pamba created, whose `list_accounts` row reads `facebook_addon_eligible` true (`facebook_addon_refusal` says why not), all paid on one card, and for now each phone takes one, so rows with the same `facebook_addon_phone_group` are a choice of one. `facebook_seats` adds it to the order's new Instagram accounts, at most one each and for now one per phone they fill (`facebook_addon_unusable_slots`, a refusal says how many; an order that also names `facebook_for_instagram` counts those accounts' phones as taken, so it can take fewer), and an order to connect takes no Facebook. Both go in one order, and `purchase_account_seats` called with no seat counts lists the accounts that can take it now as `facebook_addon_accounts`, each with its `phone_group`. A post scheduled ahead of the purchase (`await_new_instagram_account`) whose order takes `facebook_seats` waits for that Facebook account too, so its reel goes to both. The Facebook account is created from its Instagram account, with the same name, birthday and, when Facebook allows, handle, and its Instagram reels cross-post to it. `purchase_account_seats` returns a checkout URL: a purchase that raises an existing subscription gets Stripe's confirm page raising its quantity with proration, and one that starts a new subscription gets a new checkout. When Stripe cannot list the workspace's seat subscriptions or open the confirm page, the tool answers `purchase_unavailable` with a message to try again, and nothing is charged or started. A checkout paid by bank transfer completes before its payment clears (usually up to four business days): until then its seats are listed under `waiting_for_payment`, nothing is created for them, and they move to `being_set_up` once Stripe confirms the payment; a payment that fails drops them. Do not buy again while seats are waiting for their payment.
- A purchase never adopts a spare account already sitting on the workspace's phones. Check for spares first: an unassigned spare links to an avatar free with `reassign_account` and no `from_avatar_project_id` (TikTok or Instagram; no password, no phone work). The same tool moves an assigned account when `from_avatar_project_id` names its current avatar; without it an assigned account is never moved. A TikTok or Instagram account can post for several avatars (a Facebook account follows its Instagram account): `link_account_to_avatar` adds one without taking the account from the others, so a video for a second avatar can go to an account the first one already uses instead of a new purchase. `unlink_account` is the reverse: it drops one avatar's binding, and an account no other avatar links is a spare again, still signed in on its phone, with its status and history.
- A bought seat waits for its username before creation starts. `suggest_account_usernames` returns a checked, currently free idea per waiting seat (names for a waiting order are prepared in the background, so a call usually answers at once; otherwise a few seconds each, so for a large order pass `batch_size`, for example 8, and call again with `exclude` set to the names already offered; a batch answers within 90 seconds), `check_account_username` verifies a pick (availability is a hint, not a reservation), `set_account_username` saves it and starts that seat's creation, or `use_default_name=true` creates with a platform default. The account's name goes with the pick: with `display_name_from_username=true` (the app's default; leave it out to keep TikTok's own) a TikTok account's display name is set to the username it ends up with, in its own edit once the rename lands, which spends TikTok's one display-name change per 7 days; an Instagram account is created under `first_name` and `last_name` (30 characters together at most). `suggest_account_usernames` offers each Instagram seat a name with its username, the linked avatar's or else the username's first name (Claire for `wanderwithclaire`) with a surname picked for it, and `check_account_username` shows that name for any Instagram handle. A seat saved without a name is created under the avatar's name (a one-word one with a surname drawn at signup), else its username's, else (no username) a generated one, so pass an offered name back with the username it came with to keep it, and never send a typed handle's name on a seat linked to an avatar. An account made on a phone that already holds an Instagram account, where Instagram asks for no name at signup, gets a name given for it (an offered one sent back included) or its avatar's about three days after it is made, a name changed in the meantime winning, and no name otherwise. Instagram gets the typed name at signup; TikTok assigns its own handle and applies the pick as a rename right after, which spends TikTok's one username change per thirty days; until the rename lands (usually a few minutes) the account stays off `list_accounts`' `accounts`, listed under `accounts_being_named` with its pick, and its seat shows in `purchase_account_seats`' `being_set_up` at stage `applying_username`. A name that fails when applied falls back to close variations, then keeps the platform handle; the account-ready email and Home item then say which pick was not used, and `list_accounts` carries it as `picked_username_not_applied` for thirty days or until the account is renamed. Progress shows per account on `list_accounts` until the account is ready.
- `cancel_account_seat` keeps the account posting until the paid period ends, then stops posting and billing, no refund; `reinstate_account_seat` undoes it before that date at no cost, the seat billing again from the next renewal; a seat subscription cancelled on Stripe's billing page renews with it, its other seats included. Cancelling an Instagram seat cancels its Facebook add-on with it, on the same date, and reinstating it brings the add-on back; a Facebook add-on can be cancelled on its own, and reinstated on its own only while its Instagram seat is kept. An Instagram account with a Facebook add-on can't be recreated for now (`has_facebook_addon`). Once a workspace holds no seats, the phones its seats came with are signed out and returned to the pool three days after its last seat subscription ends, even while a credit plan runs. When a whole seat subscription ends (its seats cancelled, or Stripe gave up on a failed payment), its accounts stop posting at once but stay on their phone for those three days: `get_seat_billing` lists them under `restore_offers`, and `restore_account_seats` returns a checkout that brings them all back at the price that subscription billed (an account the workspace had paused itself comes back paused); after that, resubscribing can't bring them back. An offer's `payment_failed` is true when Stripe ended the subscription because its renewal payment didn't go through; Pamba then voids that subscription's unpaid invoices, so the old bill can't be paid for nothing. `get_seat_billing` shows the next renewal's total per seat. `recreate_social_account` replaces a seat-funded Instagram account with a new one on the same seat, on its phone unless that phone takes no new accounts, when the new account goes on another of the workspace's phones or a new one (once every 10 days per seat, admins only; the old account keeps counting in the workspace's analytics with nothing new read for it, flagged `archived`, its videos are not deleted and Pamba keeps its credentials in case they are ever needed; its pending posts move to the new account, or with `move_scheduled_posts: false` are cancelled when the new account lands and their videos stay to schedule again); `get_account_recreation` says whether an account can be recreated now and how a recreation is going. A TikTok account cannot be recreated by the workspace (`unsupported_platform`) unless TikTok banned it and it carries the free `replacement_offer` below: otherwise only Pamba's team replaces one. While any recreation runs, `list_accounts` leaves the old account out and lists it under `recreations_in_flight`.
- `import_social_account` brings a TikTok or Instagram account the user already owns onto Pamba: a seat still waiting for an account takes it at the normal seat price, one bought to connect before one bought to create (`no_open_seat` means buy seats with `purchase_account_seats` and purpose `connect` first: the same prices and pair discount, no usernames asked, and each seat waits in `being_set_up`, purpose `connect`, until an import fills it, so ten accounts are paid for once and connected one by one, whenever the user is ready), and one of the workspace's phones signs it in with the handle, password and, when it is not the handle, the email or phone the user signs in with. Accounts that sign in only with Google or Apple need a password set in the app first. The sign-in usually stops on a verification code the platform sends the user: poll `get_account_import_status` every few seconds, and the moment its `code_request` is set (`kind` email, sms, authenticator or approve_on_device, the masked `destination`, `wrong_code` after a refused code, `expires_at`) ask the user for the code and pass it to `submit_account_import_code`; codes expire within minutes, and `resend_account_import_code` asks the phone to send a new one (after `resend_available_at`: the apps hold their own resend about a minute). For an email or text code the app also asks on the user's own phone whether the sign-in is theirs, and approving there works with no code. `password_rejected` true means the app said the password was wrong and sent the code to an address it chose: the user enters it only if the address is theirs, and if it is not, the handle reached someone else's account (`cancel_account_import` and start again with the right handle, or, once the code wait ends and the import reads `needs_customer`, `retry_account_import` with the right `username`). `needs_customer` carries a `failure` the user must fix (a wrong password, a code never given, too many tries, a suspended account, or `passkey_required`: TikTok only lets the account in with a passkey on the user's own phone, which they remove in TikTok under Settings and privacy, Account, Passkey; or `approve_on_device`: the app asked them to approve the sign-in on their own phone, so they retry when that phone is with them and approve it there), then `retry_account_import` with the new password, login or handle (`username`: a mistyped handle reads `account_not_found`; a retry after five sign-in attempts hands the import to Pamba's team instead). `failure.code` `wrong_account` means the user's login opened another of their accounts, named in `opened_username`, which the phone has already signed back out: ask whether it is the one they meant, and if so `retry_account_import` with `username` set to it (the same login and password), otherwise with the `username` and login of the account they meant (a reconnect leaves `opened_username` empty and keeps its handle); `with_team` means Pamba's team is finishing the sign-in by hand and settles any code it needs with the user directly, or hands it back as `needs_customer` when only the user can move it on. `cancel_account_import` stops an import and frees its seat; a reconnect is refused (`reconnect_not_cancellable`) while its account still fills its seat, and Pamba closes one 3 days after its seat ended, when the seat can no longer be bought back. Pamba changes nothing on an imported account by itself (no AI-comment filter, no AI-creator label, no rename); it warms like every account on a phone. If TikTok or Instagram later signs it out of its phone (a password change, or the app ending its session), its import reopens as `needs_customer` with `failure.code` `signed_out` and the user is emailed: until `retry_account_import` reconnects it (the stored password unless the user gives a new one, then the code the app sends, as on the first connect) it can't post or warm, `schedule_post`, `schedule_instagram_post`, `schedule_slideshow_post`, `post_instagram_story` and `post_tiktok_story` refuse it with `account_needs_reconnect`, its scheduled posts fail with that code (retry them once it is reconnected), and `list_announcements` leads with a warning (kind `warning`, label `Signed out`) naming it. An import connects only the account it names: Instagram also signs in every account that shares a Meta Accounts Center with it, and Pamba never lists, warms or posts from those; connecting one takes its own seat and import.
- **A workspace that only buys accounts and posts its own videos** runs this order, and nothing in it spends credits: `list_accounts` to rule out a spare, `purchase_account_seats` (pass `link_avatar_project_id` when the account is for an avatar) and the person pays at the checkout URL, `suggest_account_usernames` then `set_account_username` per seat (creation starts only once a seat has a name), `list_accounts` until the account reads `posting`, then `create_draft_project`, `set_edited_video` and `schedule_post` with the new username (see Scheduling and posting). To queue the post before the account exists, the project needs the avatar the purchase links to, and `schedule_post` takes `await_new_account=true` in place of a username.
- `follow_accounts` has a TikTok or Instagram account on a Pamba phone follow a list of usernames or profile links. An account's lists keep to a daily limit, because TikTok and Instagram can flag an account that follows too much: none in its first seven days on a Pamba phone (counted from its first warming session or follow list), so the first follow lands once the account is a week old, then one a day at most, however old the account. Warming's own follows are separate and do not count toward it. `get_account_follows` reports the limit today as `daily_limit` (with `starts_at_utc` while a new account's first days last). A list waits for the limit, and a waiting list's next session time says when it continues. The phone follows one account per session, at a different time each day between 8 AM and 1 AM Pacific, around the account's posts and warming, and at most fifty wait at once; a session that followed no one (already followed, not found) leaves the day's follow for the next session, at least three hours later (up to half an hour more). A request while a list is running joins it (up to a hundred accounts on one list in all; a list being stopped takes nothing new, so the request opens the next list), and a new list starts no sooner than three hours after the account's last session. A profile the account already follows is left alone, and the account never follows itself. It runs in the background: `get_account_follows` shows the running list with each account's outcome (`followed`, `requested` for a private account, `already_following`, `not_found`, `failed` (where a mistyped or deleted username usually lands, with a reason to check it), `cancelled` when a stop reached it first, or `skipped` when the request failed before reaching it), and `cancel_account_follows` stops what is still waiting; a session already on the phone finishes its one account first.
- `delete_account_posts` deletes published posts from an account on a Pamba phone, videos and slideshows alike (TikTok videos and photo posts; Instagram reels, carousels and pictures), up to twenty per request and fifty waiting per account: it removes them from the platform itself, so confirm the exact posts first. Name each by its `post_id` or by its link: a project's `posted_tiktok_video_url` or `posted_instagram_video_url` deletes that project's post, and a slideshow post's posted link deletes that slideshow post. `list_account_posts` lists the account's posts newest first with each one's `post_id`, its `kind` (`video`, `slideshow` or `picture`) and its deletion state (`pending`, `deleting`, `deleted`, `not_found` when the post was already gone, `not_confirmed` when the phone could not prove the post on screen was the one asked for and deleted nothing, `delete_unconfirmed` when the phone may have deleted it but its link did not show it gone (the delete was confirmed, or the post was in a session nobody heard back from), which is not final: Pamba checks the account's own listing about fifteen minutes after the session and settles the post `deleted` once the account no longer lists it, or `failed` when it still shows it (a post the listing cannot see, such as an Only-you TikTok post or an Instagram reel off the profile grid, is settled by Pamba's team instead), `failed`, `cancelled` or `skipped`). A post the phone reported `deleted` that the account still lists on both checks turns `failed` too. The phone deletes five per session, around the account's posts and warming; TikTok and Instagram keep a deleted post 30 days in Recently deleted, where only someone on the phone can restore it. With `post_ids` (ids or links) it returns only those posts, and still returns a deleted post the platform no longer lists, with its state. `cancel_post_deletion` stops what is still waiting; a session already on the phone finishes its own few first. In the app it is Delete from TikTok or Delete from Instagram on a posted post (the project's More menu, and the post's row and calendar card on the Schedule page, slideshow posts included).
- `list_comments` lists the comments on the workspace's own TikTok and Instagram videos from the last sixty days (only the accounts it pays for, on its Pamba phones or under a live seat; an account it only tracks for analytics has none), each top-level comment with its replies and its video: newest thread first, or with `sort` the most liked first (`most_liked`) or in the order TikTok or Instagram itself ranks the video's comments as of Pamba's last read (`top`). Comments are read about every fifteen minutes once a video's comment count grows, so a new one shows within about an hour; `checked_at` is when Pamba last checked the accounts' videos for new comments, `synced_at` when it last read one of the listed videos. `hidden_by_platform` marks a comment Instagram hides under "See hidden comments", and the replies under it: replying to it, liking, pinning or deleting it is refused (`comment_hidden`), since the phones never find it there, and comment automations skip it. `questions_only=true` keeps the comments that ask the creator something or ask for something the video offered, `mine=true` keeps the threads the workspace's accounts took part in, and `q` searches the text, the authors and the replies. `about_ai` marks a comment that says or asks whether the video or the person in it is AI or not real, and each TikTok account on the workspace's phones carries `ai_comment_block_state` and `auto_block_ai_comments`: when such comments sit on an account whose filter is `failed`, or `off` with `auto_block_ai_comments` true (false means the user allowed AI comments), `update_account_profile` with `disable_ai_comments` and `auto_block_ai_comments` both true turns on TikTok's filter for them, as the Comments tab's Block AI comments line does. `list_comment_videos` gives the same comments per video, newest activity first, with how many match and how many are questions, for going through them one video at a time; it also lists every post from the last seven days before its first comment, so the account can leave that first comment with `comment_on_video`. `reply_to_comment` answers a top-level comment and `comment_on_video` adds a new comment to one of the workspace's own videos, from any of the workspace's accounts on that platform (`account`, the video's own by default); either is typed within minutes on that account's phone, or at `scheduled_for` (an ISO-8601 time with its offset, from a minute to thirty days ahead), so only an account on the workspace's phones can send (the `accounts` list says which `can_reply`), up to one hundred fifty characters on TikTok and two thousand two hundred on Instagram, and at most thirty in any rolling day per account. On TikTok an @username in the text is picked from TikTok's own list as the phone types it, so it posts as a tappable mention showing that account's display name; a username TikTok does not offer fails the comment unsent (`failed`, with a reason naming it), since typed as it is it would not link. Instagram links a typed @username by itself. TikTok and Instagram lock an account that comments on other accounts' videos too early or too often for suspicious activity, so on another account's video (another of the workspace's), an account Pamba created comments only after its first seven days (status `new_account`), and any account at most the workspace's `cross_account_per_day` in any rolling day (one unless the Pamba team raised it; `cross_account_daily_limit`); each account in `accounts` that can reply carries `cross_account_blocked_reason` and `cross_account_available_at` while it is held, and its own videos are never held. Two accounts signed in on the same phone never comment on each other's videos, nor reply to or like a comment on them or one the other wrote (`same_phone`, at any time); each account's `phone_mates` names the accounts on its phone. A comment, reply or like that reaches the phone anyway, because the two came to share a phone after it was asked for, is cancelled before it is sent, its `status_detail` saying why. Every limit counts at the time the comment goes out, against the account's comments before and after it, so a refusal's `available_at` passed as `scheduled_for` queues it, and a workspace spreads comments across days by scheduling them. What an account is asked to do now on one video, comments, replies, likes, pins and deletes alike, goes out together in one phone session, as a creator does it in one sitting: each request waits two minutes for the next, the run goes out at most ten minutes after its first, and a session takes up to ten; requests given `scheduled_for` go out at their own times, so the same time groups them and different times keep them apart. `comment_on_video` with `pin` true has an Instagram reel's own account pin the new comment once it is live. `like_comment` likes a top-level comment from one of the workspace's accounts (the video's own by default on its own videos) and `pin_comment` pins one on the workspace's own Instagram reel from the reel's account (Instagram keeps up to two pinned comments per post; TikTok no longer lets creators pin comments, `pin_not_available`), and `delete_comment` deletes a comment or a reply from the workspace's own TikTok video or Instagram reel through the video's own account (only a video's owner can delete other people's comments on it, so never on a video the workspace added, `delete_not_allowed`; nor a comment with no words of their own (only a sticker, photo, GIF, @mentions or punctuation), or a reply under such a comment, `not_deletable`; it cannot be undone, a comment's replies go with it, it withdraws the workspace's queued replies, likes and pins of the comment and deletes of its replies, and the comment takes no more while the delete is on its way, `comment_being_deleted`, also the answer for a reply whose comment is being deleted; a reply's delete shows in its thread's `reply_actions`, and once the phone saw the comment gone `list_comments` no longer lists it); all three are typed on the phone like a comment, likes and pins now or at `scheduled_for` and a delete now, thirty a day per account together, never counted against the comment limits; a like from an account that does not own the video (another of the workspace's accounts' videos) keeps to the workspace's `cross_account_likes_per_day` in any rolling day (one unless the Pamba team raised it; `cross_account_daily_limit`, with `available_at`), each account in `accounts` carrying `cross_account_like_blocked_reason` and `cross_account_like_available_at` while it is held, so likes in bulk are spread across days by giving each its `scheduled_for`; and `list_comments` shows them in each thread's `actions` (`list_outgoing_comments` lists them with `action` like, pin or delete; it lists comments alone by default). To reply in advance, pass `reply_to_comment` the outgoing id of one of the workspace's own comments that is not live yet: the reply waits for it, and up to three replies can wait behind one comment, going out one at a time in the order they were asked for, each once the one before it is live and fifteen to forty-five minutes after it was sent (`reply_to_outgoing_id` names the comment it answers and `waiting_for_id` the one it waits for). Once a comment has replies in advance behind it (on their way or live), another reply to it, by its outgoing id or by its comment id once it is live, joins the end of that line. If a comment before them fails, is cancelled or still does not show two days after it was sent, the replies behind it are cancelled too; a fourth reply answers `chain_full`, and a reply to a comment that will not go live answers `parent_not_live`. `list_outgoing_comments` follows each one: `queued`, `sending`, `posted` (the phone sent it, or Pamba lost the phone's answer; a read of the video settles which), `live` once a read of the video shows it, `not_visible` when no read has six hours later, or `failed`; `cancel_outgoing_comment` withdraws one while it is still queued, with the replies waiting behind it.
- `create_comment_automation` answers comments by direct message: when someone comments one of its `keywords` on one of the workspace's TikTok or Instagram videos (its `accounts`, or with `all_accounts` every account on the workspace's phones, those added later included; every post), the account's phone reaches them from their comment (it opens the post, finds the comment and taps their name) and sends them `dm_text`, a first message with no link that asks them to reply (a link in it is refused, `link_in_first_message`), once per person; once they reply yes (or anything that says they want it), within seven days, the phone sends `follow_up_text`, the message with the link (required, `follow_up_required`; a few words with the link: one that is only a link is refused). Instagram shows a link to someone who does not follow the account only after they reply, and waiting for a reply keeps the account from looking like spam. Then, in the same visit, replies under the comment with one of its `reply_variants`, or `fallback_reply` when the person's settings refuse the message (a reply counts toward the account's thirty comments a day; one the comment queue refuses is left out). A keyword matches as whole words, ignoring capitals, accents, punctuation and emoji (`match_mode` `exact`: the comment is the word and nothing else), and `{username}` in the message or a reply becomes the commenter's @handle. Each account sends up to twenty first messages a day, spread out through the day (`list_comment_automations` reports the limit as `daily_dm_limit`; the messages with the link do not count toward it), on TikTok the oldest comment first and on Instagram each post's newest waiting comment first, the posts taking turns (a comment the phone cannot find ends `not_sent`, `comment_not_found`, and does not count toward the limit); a TikTok message goes out as one paragraph. It is on at once (`enabled` false: created off) and also answers matching comments from the last seven days unless `answer_recent_comments` is false; no automation answers a comment older than seven days, or one by the workspace's own accounts. `update_comment_automation` changes any field and turns it off or on (`enabled`); turned back on, or set to answer other comments (its keywords, match mode or accounts), it answers only comments made from then on, and a new `dm_text` or `follow_up_text` goes to the messages and people still waiting too; while it is off, the links of people who said yes wait as well. An automation that is off can have no `follow_up_text` (one saved before the two messages whose message had no link to send second): turning it on needs `follow_up_text`. Two automations that are on cannot share a keyword on one account (`keyword_in_use`), and a comment that says the words of two of them gets one message, the older automation's. `list_comment_automations` lists them with their last seven days and the accounts that can send; `list_comment_automation_activity` lists every comment one answered and its message's status (`queued` with `next_attempt_at` while it waits for the daily limit or the account's next session, `sending`, `sent`, `not_sent` with why, `skipped` for someone already messaged or, on Instagram, a commenter whose profile is private (no message is tried, it does not count toward `daily_dm_limit`, and the comment gets `fallback_reply`), or a comment Instagram hides (`comment_hidden`: the phone never finds it, so nothing is sent), `cancelled` when the account's seat ended) and, once the first message went out, where the link stands (`follow_up_status`: `waiting` for their yes, `sent`, `not_sent`, `no_reply`, `cancelled`, with `follow_up_detail`; `replied_at` is their yes), and `list_comments` marks each answered comment in its thread's `automations`. `delete_comment_automation` deletes one; its waiting messages and links are not sent, and what it sent still counts toward the account's daily limit.
- `update_account_profile` edits display name, handle, picture and bio on the hosting phone. Bios are capped at one hundred sixty characters on TikTok and one hundred fifty on Instagram; a URL typed into a bio is plain text, the link field is separate and untouched. On TikTok the phone picks each @handle in a bio from TikTok's mention suggestions, so it lands as a tappable mention that TikTok shows as @ and that account's display name; a handle TikTok offers no account for fails the bio field with the error code bio_mention_not_found, and a mention the phone could not show links the account asked for (a pick on a suggestion whose caption TikTok cut short, or a bio TikTok already held whose mentions are not drawn as links) with bio_mention_unconfirmed; an email address stays text. `list_accounts` reports each account's current bio, whichever is newer of the one Pamba last set and the one last read off the phone (on TikTok, a read that is the bio Pamba set with each mention shown as @ and a display name still reports the bio as set, with the handles an edit sends again); every linked account's bio is read once a day, so an in-app change shows within a day. Only the fields an edit asked for count as outcomes: a bio-only edit reports one operation, not three. On TikTok the phone gets time for each field and each @handle an edit asks for; a field it had no time left to start comes back not applied with the error code field_out_of_time, beside the fields it did apply. An Instagram bio with a link, put on an account Pamba created less than seven days ago, still goes through, and the result carries a `warning`: Instagram often restricts link sharing on accounts that add a link this early. A `queued` answer, from this tool or from `enable_instagram_ai_creator` and `disable_instagram_ai_creator`, carries the edit's `job_id`, which `get_account_profile_edit` reads to its outcome.
- `update_account_profile` also switches an Instagram account between personal and creator with `instagram_account_type`, sets its profile category with `instagram_category` (any case, one of Digital creator, Artist, Musician/band, Blogger, Clothing (Brand), Community, Education, Entrepreneur, Health/beauty, Writer, Personal blog, Product/service, Gamer, Restaurant, "Beauty, cosmetic & personal care", Grocery Store, Photographer, Shopping & retail, Reel creator, Gaming video creator, Social Media Agency, Camera/photo, Computer Company, Makeup Artist or Creator) and whether the profile shows it with `instagram_show_category`, sent in a call of their own with no other field: the account type is its own job on the phone, answered `queued` with a `job_id` that `get_account_profile_edit` reads (its kind is instagram_account_type, its fields account type, category and show category). Pamba never switches an account to business, since Business accounts can't use most of Instagram's licensed music: `business` is refused with the error code instagram_account_type_invalid, and an account that is a Business account already is never switched to creator (instagram_account_business) but can go to personal or change its category; when the phone finds a Business account Pamba had not read yet, the job fails with ig_account_business and nothing changes. `creator` on an account that is creator or business while its switch to personal still waits for the phone, never yet sent to it, withdraws that switch instead: the call answers `withdrawn` with `withdrawn_job_ids` and the account stays as it is (a category or display in the same call is queued as its own change); a switch to personal already sent to the phone is refused with change_in_progress, and on a creator account after an earlier type switch of it ended unread the call is queued over the waiting switch instead (it answers `queued`), for the phone to read the type the account has. A personal account, or one `list_accounts` gives no `instagram_account_type` for, switched to creator takes the category Digital creator unless the call (or a change already queued) names one, and a hidden display unless it names one; an account that is creator or business already changes only the fields the call names. A category or its display alone needs an account that is creator or business, or has a switch to creator queued (one Pamba knows is personal, or with a switch to personal queued, is refused with the error code instagram_account_not_professional; one whose type Pamba has not read goes to the phone, whose job fails with ig_account_not_professional when the account is personal), a category Pamba does not offer answers instagram_category_invalid with the list, and a call for TikTok is refused, since TikTok's only switch is to a Business account, which limits it to TikTok's commercial music library. Switching makes the profile public, and Instagram also turns on stricter filtering for professional accounts, which hides more comments and message requests; going back to personal turns its insights off. `list_accounts` reports each Instagram account's `instagram_account_type`, `instagram_category` and `instagram_show_category`, the newer of what Pamba last set and what the hourly analytics refresh last read. Instagram trial reels need a creator or business account with at least two hundred followers.
- `update_account_profile` also keeps a Creator or Business Instagram account's audience in chosen countries with `instagram_audience_countries`, in a call of its own: `us_only`, `tier1` (the US, Canada, the UK, Australia, New Zealand, Ireland, Singapore and Western Europe), `tier1_tier2` (adds Central, Southern and Baltic Europe, Japan, South Korea, Israel, the Gulf, Hong Kong and Taiwan) or `all` to clear it. It works through Instagram's Minimum age setting: outside the chosen countries Instagram shows the account only to people it knows are 25 or older, so it does not block a country. A personal account is refused with `instagram_account_not_professional`. The change is its own job on the phone, answered `queued` with a `job_id` that `get_account_profile_edit` reads (its kind is instagram_audience_countries, its field minimum_age, whose applied value is the choice the phone read back); it takes a few minutes for `us_only`. Instagram takes only about 15 such changes a day on an account, so a bigger change continues by itself the next day (`tier1_tier2` from scratch takes three days); between runs the job reads queued, and a choice sent meanwhile joins it. `list_accounts` reports the account's `instagram_audience_countries`, `custom` when it was set by hand in Instagram, and none after a change that failed partway, until the next change reads the page again.
- Pamba-created TikTok accounts block AI comments automatically once linked to an avatar: an account linked at creation gets the filter after its first post is live, one linked later gets it queued at link time, and an unassigned account is never filtered. `disable_ai_comments` turns it off per account. `get_avatar` reports the state per account as `applied`, `queued`, `scheduled`, `failed` or `off`, and null when the account is not on one of the workspace's phones.
- `enable_instagram_ai_creator` and `disable_instagram_ai_creator` turn the label on and off; each leaves a label already where it asks untouched, and a change still queued for a busy phone takes the newest call's direction. While the phone is applying one direction, a call for the other is refused with `change_in_progress`; call again once it settles. `list_accounts` reports each Instagram account's `ai_creator_label_state`: `turning_on` or `turning_off` while the phone is queued or running to change it, then `on` or `off`, `failed` when the newest attempt to turn it on failed, or `turn_off_failed` when the newest attempt to turn it off failed (the label is still on); each TikTok account carries its AI-comment filter as `ai_comment_block_state`. Both are empty for an account on none of the workspace's phones.
- Warming (human-like browsing on the account's phone, so it reads as a real person before and between posts) is always on for every device-hosted account and cannot be turned off from the tools; Pamba sets its pace, which is not configurable. Every account warms under its own config (`get_account_warming_config`, `set_account_warming_search_terms`), whichever avatars link it. An avatar's config (`get_warming_config`, terms via `update_avatar_settings`) is the one its accounts start from: an edit there is written onto every account the avatar links, shared ones included, and an account linked to its first avatar takes that avatar's settings. A linked account warms while every link of it posts and at least one linking avatar is active. Search terms steer what warming looks for. Terms cap at twenty-five of eighty characters, and a save carrying a term with "porn" is refused with a message naming it, since TikTok and Instagram show no results for it; without explicit terms, sessions fall back to terms generated from the workspace niche (`search_terms_source` niche), while an empty list saved with `use_niche_terms` false is kept as the owner's own and its sessions search nothing (`search_terms_source` none; `use_niche_terms` true with an empty list goes back to the niche terms). A term TikTok answers with its guidelines page on the phone leaves the account's terms, the terms of the avatars linking it and of every account those avatars link, and the niche terms by itself, and the workspace gets a Home item at once and an email about two hours later; a "No results found" on either app removes nothing. The workspace's defaults, which a new avatar and a new TikTok or Instagram account with no avatar start with (an account added to an avatar takes the avatar's setting), are `update_workspace_settings` `warming_searches` and `warming_follows` (`list_workspaces` reports both); either needs `apply_warming_to_existing_accounts`: true also writes it onto every existing account and avatar, false changes the default for new ones only, and without it the call changes nothing and answers `needs_choice`. Warming keeps running either way. `set_account_warming_switches` sets one account's own switches: `follows_enabled` false and its sessions follow no one, `searches_enabled` false and they never search while its terms are kept for when searches are back on; `get_account_warming_config` reports both. `update_avatar_settings` with `warming_searches_enabled` or `warming_follows_enabled` sets them for every account an avatar links, shared ones included, and for an account it links later that no other avatar links; `get_warming_config` reports what the avatar's last edit set, as it does its terms. An Instagram account's Cross-post reels to Facebook switch rides the same config and is set two ways, with no workspace default (a new account starts on): `set_account_facebook_cross_posting` for one account, `update_avatar_settings`' `facebook_cross_post_enabled` for an avatar's Instagram accounts; `get_account_warming_config` and `get_warming_config` report it as `facebook_cross_post_enabled`. `cancel_warming_session` works only on pending sessions. `get_account_activity` is one feed per account of what the phone did for it, newest first: warming sessions (status, length, videos watched, likes, comment reads, creators followed, the search term, a few sample screenshot URLs), daily TikTok health checks, posts, profile edits, finished follow requests and post deletions, and the comments and replies the account posted with every screenshot the phone took of each; pass a page's `next_before` back as `before` to read further back, and an empty `next_before` ends the feed.
- Every device-held linked TikTok account gets one account-health check a day. Findings open incidents (`ACCOUNT_WARNING`, `VIDEO_REMOVED`, `VIDEO_NOT_RECOMMENDED`), shown as health flags and cards and surfaced on Home, and emailed to the workspace about two hours later; these kinds never pause the account. `get_tiktok_account_health` returns the latest screenshots and open issues with each appeal's progress; `request_tiktok_appeal` records the decision to appeal a removed or restricted video or an account warning, one appeal per issue, and nothing is appealed without it unless the account's auto-appeal is on (`set_tiktok_auto_appeal`, reported by `get_tiktok_account_health` as `auto_appeal`): then every open issue is appealed when it goes on, and each new one as the daily check finds it. The account's phone then files the appeal itself, submitting only once a TikTok screen prints that video's posted time (a video it cannot pin down is filed by the Pamba team by hand; an `ACCOUNT_WARNING` is appealed through the video TikTok removed with it: appealing it also appeals the one removed video the same daily check found, and one verdict decides both; with none or several, the Pamba team files it by hand), and reads TikTok's verdict off the account's notifications once a day until it answers: `appeal_verdict` APPROVED closes the issue as restored, REJECTED keeps it open. An appeal TikTok has not answered four days after filing sends the phone back to the video to read TikTok's answer on its page. A removed video reappearing also closes its issue and approves its appeal, a video the phone finds no longer restricted when it goes to file closes its issue as restored, other issues close after two clean daily checks, except one whose appeal is still being filed or still waits for TikTok's verdict (up to 21 days after filing). A video the workspace deletes through Pamba (`delete_account_posts`) takes the open issues that predate the deletion with it once the deletion confirms it gone (closed as `DELETED`): its appeal stops wherever it stood, and nothing is filed, read or rechecked for it. An `ACCOUNT_WARNING` TikTok gave for that video stays open; if its appeal was not filed yet, the Pamba team checks whether TikTok still takes it. When Pamba replaces an account with a new one, every open issue of the old account closes (closed_by `MANUAL`) and its appeals stop. `close_account_incident` dismisses an issue for good.
- Accounts suspended by Meta open a `SUSPENDED` incident that stops Pamba's warming, daily reads and posting on the account until the incident is closed with `close_account_incident`, only after the account is verified restored: its warming sessions are cancelled, its pending posts fail saying why, and every new post, retry, profile edit, comment or other request on it is refused, as on an account TikTok or Instagram banned (below). A suspended Instagram account is signed out of its phone, so Meta's page stops blocking the phone's other accounts: it stays listed on its phone and is signed back in once its incident is closed. Its phone then takes no new Instagram account (a banned account's phone takes no new account of its platform either), so `recreate_social_account` refuses the suspended or banned account itself with `phone_on_hold` (Pamba's team replaces it on another phone) and makes the new account of any other Instagram account on that phone on another phone: another of the workspace's phones with room, else a new one, the old account staying where it is. One exception: an Instagram account suspended by Meta, or asked by Instagram for identity verification with a video selfie (an `IDENTITY_VERIFICATION` health flag, its incident's `error_code` `ig_identity_verification`), after its first day can be replaced for free, and so can a TikTok account TikTok banned after its first day (its new account goes on a phone that never hosted an account). `get_account_recreation` and `list_accounts` report it as `replacement_offer`, and `recreate_social_account` then takes it with no cooldown and makes the new account on a new phone (`suggest_account_usernames` with `replacing_platform` and `replacing_username` offers free usernames for it; a seat gets one free replacement a month, and without an offer the account is the team's to replace; `posts_start_after_days` starts the moved posts at least a few days after the new account is ready, each at its own time of day). An account Pamba confirmed TikTok or Instagram banned carries a `BANNED` health flag and incident (a TikTok ban about an hour after Pamba finds it, before which its work waits): it takes nothing more, its pending posts fail saying why and every new post, retry, profile edit, comment or other request on it is refused, so schedule its videos again on another account or the new one; a TikTok one past its first day carries that free `replacement_offer` for the user to take, otherwise Pamba replaces it with a new account on its seat at no extra cost, and only Pamba closes the incident (`close_account_incident` answers `pamba_closes`), with `closed_by` `RESTORED` when the account comes back or `MANUAL` once a new account replaces it. While any account is banned, `list_announcements` leads with a warning (kind `warning`, label `Banned`) naming them, the strip shown across the top of the app; a `Signed out` warning about accounts to reconnect comes after it. A ban outranks a `SUSPENDED` incident on the same account: don't suggest an appeal or close the suspension. Instagram can also restrict one feature of an account that keeps working ("You can't share links" until a date): the phone closes Instagram's restriction page and Pamba opens a `FEATURE_RESTRICTED` incident whose detail carries the restriction and its `ends_on`. It never pauses posting or warming, shows through its end date and closes the day after. `reconcile_tiktok_rename` repairs an account renamed on TikTok itself, moving the avatar's active account and all forward-looking drafts, plan and scheduling to the new handle; old posts stay under the old handle.

### Ideas, discover and research

- `list_ideas` and `get_idea` read the library (twenty-five per page, sortable by date, views and engagement ratios); every hub video carries a replication `feasibility_verdict` (`pass`, `fail`, `unjudged`) with the judge's reason, and `exclude_low_feasibility=true` hides failed ones while unjudged stay visible. `get_idea` lists `required_brand_assets` with `in_library` per kind, which tells you before replicating whether an upload is needed. A workspace holds up to one hundred ideas.
- Research needs an active subscription, a credit plan or account seats; free workspaces browse but every addition (`create_idea`, `branch_idea`, `add_tracked_accounts`) answers `payment_required`. `generate_script_from_idea` also runs for a workspace holding credits it was given or bought, while any are left. Saving a video that is already analyzed is allowed on every plan. The inspiration-account cap scales with the plan tier. Tracking an account feeds its breakout videos into the create page's For You blend once the feasibility judge passes them; `remove_tracked_account` cannot remove an account linked to one of the workspace's avatars.
- `search_tracked_videos` searches the workspace's own accounts by default (avatar-linked, signed in on its phones, or with a post it scheduled); pass `include_research_accounts=true` for the inspiration accounts. Ground recommendations there rather than in memory. `generate_script_from_idea` writes a fresh script, thirty to forty-five seconds unless the idea asks for another length and in the idea's language, and does not save it; `save_idea_script` or `create_video_project` does.
- Discover: `get_trending_videos` (by default only avatar-replicable content, screened for promotion carriage; `include_unproducible=true` reveals everything), `get_niche_feed` (returns `niche_status` `pending` or `no_niche` for an empty feed), `search_viral_videos` (paid only; falls through to a live TikTok search when the corpus is thin, with a daily budget reported as `live_searches_remaining_today`), `get_breakout_creators`. To act: `use_discover_video` puts a video on the Ideas board (idempotent), `bookmark_discover_video` saves it, `track_video_author` adds the creator as an inspiration account. `browse_picker_videos` is the create page's shelf; a row whose `video_url` is null is an un-enriched Discover row, so prepare it with `use_discover_video` first.

### Content review, knowledge and automation

- Campaigns are autonomous cycles that plan, write, generate and queue posts for their member avatar and account bindings on a schedule, gated by review windows unless auto-approve is on. Schedules run in the workspace timezone and plans are ready for review by nine in the morning local time by default. Every workspace has a Default campaign that owns unassigned members and cannot be deleted; `create_campaign` copies its schedule and starts disabled with no members; `assign_avatar_to_campaign` is a complete move. `enable_campaign` spends credits on every future cycle until `disable_campaign`, and both need an active subscription on the workspace owner. `run_campaign_now` plans the gap days up to the next scheduled cycle without stealing that cycle's window.
- `preview_content_plan` is a dry run that creates nothing; `plan_content_batch` plans (or re-plans an unapproved batch via `batch_id`); `get_content_plan` explains, per avatar, why each got its slots; `edit_plan_slots` moves, removes, swaps, adds or pauses. `approve_content_plan` or `approve_all_content_plans` is where credit spend starts, and approval is refused while the plan has error-severity violations. `set_mix_targets` replaces all targets, so read `get_content_strategy` first. `delete_plan` only removes a planning or brief-pending batch and never touches projects.
- `push_content_batch` is safe by default: without `push_now=true` or a `preflight_id` it only runs the preflight. With `push_now`, each pushed approved script becomes a fully automated project that generates and schedules, and spends video credits. `decide_content_script` with a `batch_id` and no `script_id` approves every undecided script; `require_green=true` skips scripts with failing checks. `send_writer_chat_message` answers asynchronously: read the thread with `get_content_script` (`view=writer_chat`), then `apply_script_revision`.
- Knowledge base: patterns (a pattern's category is its canonical format; `lipsync` patterns are classification-only, the planner never allocates them), principles, playbooks and term lists (tiered pools of real terms the writer draws from: `proven`, `testing` and `exploratory` tiers are writer-eligible, `candidate` and `risk_flagged` terms are not). The writer's references are chosen automatically at claim time from pattern links; curate them with `manage_pattern_link` and preview with `get_pattern_references`.
- `get_studio_overview` is the one read for the Drafts and Videos tabs. Always pass `statuses`: `drafts` is the Drafts population (concept-stage items awaiting generation), the others (`queue`, `failed`, `approved`, `scheduled`, `posted`, `archived`) are the Videos tab's filter, and posted history and archived projects are reachable only here. Omitting `statuses` returns the classic queue capped at fifty with drafts sorted below everything else, which hides them on a busy workspace. `sort` (`newest`, `oldest`, `posttime`, `views`) decides which items make the window and requires `statuses`; `views` puts the items with the most views across their TikTok and Instagram posts first, with each item's count in `views`, an item with no views yet carries no `views`, and other sorts never send it (`statuses=posted&sort=views` lists your top videos); a typo in either errors rather than defaulting. `view=home` also returns the getting-live setup, where `ready_to_post` (not `account_connected`) says whether posting would actually work.

### Analytics

- `get_analytics_summary` (`days` one to three hundred sixty-five, default thirty; `platform` `tiktok`, `instagram` or `facebook`, default TikTok; optional `avatar`) gives views and followers per platform, broken down by up to twenty avatars. An inactive avatar's accounts stay in it as an archived account does, marked `archived`: their old views keep counting and nothing new is read for them until the avatar is active again. `search_tracked_videos` is per-video TikTok performance, `list_social_videos` per-video Instagram and Facebook performance, `get_tracked_account_metrics` an account's lifetime totals and cadence.
- An Instagram slideshow or picture (`media_kind` `photo`) has no public view count: Instagram shows its views only to the account that posted it, so the account's Pamba phone reads them off the post's own insights, about three days after posting and a final time about four weeks after, and `views_read_at` says when. Every post `list_social_videos`, `search_tracked_videos` and `get_post_analytics` return carries `views_source`, where its views come from: `public` (the count the platform shows publicly, read by Pamba's hourly scrape: every TikTok and Facebook post, an Instagram reel, a slideshow posted as one included) or `insights` (read off the post's insights by the account's phone: every other slideshow, and every picture). An `insights` post with no `views_read_at` has not been read yet: its views, 0 in a list and null on `get_post_analytics`, mean not known yet rather than zero views (the app shows a ? there). Its likes and comments come from the hourly read like a reel's.
- `get_post_analytics` (`platform` `tiktok` or `instagram`, `video_id` the TikTok video id or the Instagram shortcode) is one post's page: its counts with their daily history and its audience (gender, age from 18 (no age under 18 is ever served), top locations, new vs returning viewers, followers vs not, watch time, finish rate, and its retention curve: the percent still watching at each second, with TikTok's average share watched and the moment most viewers stopped, or Instagram's skip rate) and its engagement (the likes, comments, shares, saves and Instagram's reposts as the app counts them; when in the video viewers liked it, at each second, on the app's own scale: TikTok's chart runs to 100% at its most liked moment, Instagram's gives each moment's share of the likes; TikTok's most liked moment and the words its comments use most; the like, comment, share, save and repost rates, Instagram's own, or for TikTok, which prints none, the read's counts over its views with `rates_from_counts` true), and `retention_moments`, the percent still watching at 5 s, 10 s, halfway and the end with the skip rate, as `list_post_analytics` gives them, read by the account's phone off TikTok's Video analytics or Instagram's reel insights, with the screenshots behind each read. A post is first read once it has 200 views, at any age (or three days after posting with 100), and again as it passes 1,000, 5,000, 10,000 views and every doubling after, at most ten posts a day per account; `next_read` says when. `list_post_analytics` (`platform` `all`, `tiktok` or `instagram`, `days` 1-365 (default 30), `sort` `newest`, `retention_5s`, `retention_end`, `skip_rate`, `like_rate`, `comment_rate`, `share_rate` or `save_rate`, `limit` 1-100) lists the posts with a read, each read at a glance: the percent still watching at 5 s, 10 s, halfway and at the end (one second before it on a post up to 20 seconds long, the curve's second-to-last measured point on a longer one, since TikTok's chart falls to about zero in its last second), the skip rate (the app's own where it prints one, Instagram; otherwise the share gone in the first 3 seconds of the curve, with `skip_rate_from_curve` true), the share watched and the finish rate, and its engagement: the likes, comments, shares, saves and (Instagram) reposts the read counted, the share of viewers who did each (Instagram's own rates; TikTok prints none, so there each is the read's count over its views, with `rates_from_counts` true) and TikTok's most liked moment. Each post also carries the listing's own likes, comments, shares and saves. These are the Retention and Engagement columns of All Videos. Each row's `retention` is left empty there (`get_post_analytics` has the curve), and `total` says how many matched. `get_account_audience` is the account's own viewers (TikTok Studio, 28 days, read once the account has a post from the last 60 days) and followers breakdowns, read on its daily visit, with its latest daily health checks' screenshots; both apps show a followers breakdown only from 100 followers. `get_audience_summary` (`segment` `viewers` or `followers`, `platform` `all`, `tiktok` or `instagram`) is the workspace's audience across all its accounts on Pamba phones, each account counted by its size: viewers are TikTok Studio's (28 days) plus Instagram reels read in the last 28 days, followers are each app's followers breakdown; it lists the accounts folded in and when each was read, and is rebuilt as reads land. Each audience read and rollup these four tools return carries `sample`: how many people its numbers come from (viewers, or the views when the app printed no viewers, or followers on a followers read), the size from which such numbers are enough, and whether this read falls short of it, as most of a post's early reads and a small account's audience do. A read that falls short is low confidence: its shares, retention and rates can still move a lot as more people watch or follow, and the app marks an account's or the workspace's audience Low sample size (never a single post). A read with no count known carries no `sample`. These are the platforms' own estimates, as printed.
- The numbers are snapshots and the surfaces end their windows and total differently, so do not expect them to reconcile to the view. The summary tool and the app's dashboard read the same daily rows, each dated on its workspace's calendar; the summary ends its window on today in UTC, the dashboard on today in the viewer's timezone. `get_tracked_account_metrics` dates its per-day series on the workspace's calendar too. The dashboard sums daily deltas, while per-account metrics sum lifetime view counts. A TikTok video's first snapshot carries everything it earned before Pamba first read it, and those counts land on the day the video was posted when Pamba first read it within 90 days of posting; a video it starts tracking later adds only its later growth. On every platform, the views a reading finds two or more days after a post's previous one (an account not read while its avatar was inactive, a scrape outage) are spread evenly over those days rather than landing on the day the reads resumed. Accounts assigned to a deactivated avatar keep posting and accruing views but do not appear in avatar-scoped dashboards. A TikTok rename keeps one account row: the hourly refresh follows the account through its stable id (or one of its own videos) and moves the row to the new handle, so both handles' videos stay together; a rename it cannot follow leaves the old row failing until it is reconciled by hand. Instagram's headline views are the play count the app shows, and the first observation of a reel attributes its whole lifetime to one day.

### Workspace, members, onboarding and support

- `update_workspace_settings` is admins only; setting `website_url` re-scrapes the site and replaces the stored company profile (and the niche, unless it was hand-written). `update_company_profile` edits the brief field by field and leaves the others alone; an empty string clears a field. Name, niche and profile feed the model's own context, from the next connection onward. `list_workspaces` returns at most twenty workspaces and `list_workspace_members` one hundred.
- `create_workspace` with `website_url` sets a workspace up like an onboarded one in one call (brief, name, media library, niche feed, three matched avatars; the site read takes about thirty seconds); pass `niche_description`, `what_they_do` and `name` instead when there is no site (without `what_they_do` the brand stays unconfirmed and the result says how to finish it). Set up either way, the new workspace takes the goal of the workspace the call runs in (what it is for: accounts, videos or both). A new workspace has its own plan and starts with zero credits, and the connection stays bound to the workspace of its key. If setup fails after creation the error carries the `workspace_id`: finish or delete it, never create a second. `delete_workspace` is a soft delete and cannot remove the user's default workspace.
- Onboarding state (`get_onboarding_state`) runs website, brand, media, first video, avatar, plan, device. A signup wizard with `step_list_version` 2 first asks what the workspace is for (`set_workspace_goal`); for `accounts` it runs only website, brand and the referral question before `complete_onboarding`, and the accounts are bought after with `purchase_account_seats`. The `light` flow of an added workspace ends after `set_onboarding_brand` and `complete_onboarding`. `set_onboarding_website` is best-effort (an unreachable site still records the URL and advances) and kicks a background media capture into the asset library (`media_capture_status`). `list_onboarding_presenter_options` returns an empty list while the niche match is still running, so poll again rather than treating it as no avatars. `select_onboarding_presenter` picks a matched starter avatar for free; `start_onboarding_avatar` generates one and spends image credits. `create_onboarding_concept_projects` is free and idempotent. The device half must be resolved (purchased or skipped) before `complete_onboarding`, which refuses otherwise, except on that accounts track, which needs only the brand. `set_onboarding_referral` accepts only its listed sources, with free text in `detail` under `other`. A user with no credits and no avatars right after signing up has a torn bootstrap; `heal_signup_bootstrap` re-runs the missing grants idempotently.
- `contact_support` posts into the user's private support conversation with the Pamba team (up to five thousand characters, plus up to four public image links in `screenshot_urls`, PNG, JPEG or WebP, up to ten megabytes each); a user can send ten messages a minute and a hundred a day across every door, emails to support or to a team member included, and upload fifty screenshots a day. A `contact_email` stays on the conversation for later replies until another replaces it, and the result names the address replies go to. The team answers in the app's Support tab and by email, an unread answer rides every MCP tool result as a `support_reply` notice (see How a call behaves), and `get_support_messages` reads the conversation: who wrote each message, the text, screenshot links valid for an hour, and `unread_count`. Reading the newest page counts as the user seeing the replies, so the reply email is skipped. Over this transport no chat transcript is shared with the team, so put the context in the message. `list_api_keys`, `create_api_key` and `revoke_api_key` manage keys; a revoked key's MCP connection fails at its next call. `create_api_key` returns the new secret in its result, which over a hosted client lands in a transcript, so create the key for an MCP connection in the app and keep the tool for keys the agent itself will hand to a system. That includes the key `create_api_key` makes with `key_workspace_id` for a workspace the connection's key created: it lands in the transcript too, so it suits a system integration rather than a person's own connection.
- Content rules: Pamba's Acceptable Use Policy is the public page `https://pamba.app/acceptable-use-policy`; point people there for what is allowed, why a generation was refused by safety filters, and what happens to accounts that break the rules.

## Security model

- A workspace API key resolves to one user in one workspace, where workspace-bound tools run. It makes keys only there and in the workspaces it created itself (`create_api_key` with `key_workspace_id`); a chat app's connection makes keys only in the workspace the call runs in. What acts on the person reaches past the key's workspace, as it does in the app: `list_api_keys` and `revoke_api_key` cover their keys everywhere, and `delete_workspace` and the REST `/workspaces/{workspaceId}/…` routes (settings, members, invitations) act on any workspace they administer. An OAuth token resolves to one user and an explicit set of approved workspaces; each tool call resolves one approved workspace and verifies current membership before execution. Ownership checks are the same the app enforces: an id from another workspace answers `not_found`, never data. Admins-only settings answer `forbidden` for members. Operator endpoints behind Pamba's admin key are not reachable through this server at all; the admin key is not a credential here.
- Rotate a key by creating a new one and revoking the old; connections are stateless, so the next call after revocation is the one that fails. Revoking a key leaves the keys it made working, and the new key cannot make keys for the workspaces the old one created: keep a key made inside each of those, or make one later in that workspace's settings.
- If a key leaks, revoke it, then clear what it made, since revoking removes none of it. Keys: `GET /api-keys` lists every key you own with its `workspace_id` and when it was made and last used (`list_api_keys` shows the fifty newest and counts them all; a workspace's settings list only that workspace's keys). Revoke each one made since the leak, judging by date rather than name, since a key's name is whatever its maker typed, or revoke them all and make new ones. Members: a key can invite people into any workspace you administer, and an invitation link admits whoever accepts it, so check the members and pending invitations of each of those in its settings in the app, and remove what you did not add (over MCP, `list_workspace_members` and `list_workspace_invitations` read only the workspace the connection runs in); check the key's own workspace's share links too (`list_share_links`). Workspaces: `GET /workspaces` lists every one you belong to with its owner (`list_workspaces` shows twenty and counts them all); delete any you own and do not recognize, and ask the admins of one you do not own to remove you. A leaked key could also have revoked your other keys or deleted a workspace you administer, never your default one.

## Protocol details

- Streamable HTTP, stateless: every POST is independent and carries its own credential, there is no session id, and no server-to-client stream (GET and DELETE answer 405). A deploy between two calls is invisible to the client.
- One JSON-RPC message per POST, at most one megabyte (enough for a full editor state; media never travels in a message) and at most sixty-four levels of nesting; a larger body is answered with HTTP 413 and a deeper one with HTTP 400, each with a JSON-RPC error. Batches are refused. Responses are `application/json`; the server never opens an event stream. Sixteen tool bodies run at once across every connection and four per workspace: a call past the workspace's share answers `status: busy` at once, and one past the overall bound waits up to thirty seconds for a slot before answering the same, in both cases with nothing run, so a saturated server never hangs a client and one workspace's fan-out never starves another's. Protocol versions `2025-06-18` and `2025-11-25` are accepted; anything else, including `2025-03-26` (whose transport requires batches), negotiates to `2025-06-18`.
- Capabilities: tools, prompts and resources, none with change notifications; the listing changes only with a deploy. Regenerate the reference in your own docs from `tools/list` if you mirror it.
- JSON-RPC errors: `-32700` malformed JSON, `-32600` invalid request or batch, `-32601` unknown method, `-32602` unknown tool, tool outside the scope, bad arguments, unknown prompt or a missing `uri`, `-32002` resource not found (the `data` names the uri), `-32603` internal error. Auth failures are HTTP 401 before any message is read; a bad `toolsets`, `readonly` or `max_credits` value on the URL is HTTP 400 with a JSON-RPC error body.

---
HTML version: https://pamba.app/help/mcp-server · All docs: https://pamba.app/llms.txt
