# Wonda CLI Documentation (Full) > AI-powered marketing automation — generate, edit, publish, and analyze media via CLI & API. --- # Overview ## What the API does The Wonda API lets you generate images, videos, text, music, and speech; transcribe or analyze media; upload files; inspect jobs; estimate costs; and publish finished media to connected social accounts. Use REST endpoints for generation, media, pricing, publishing, analytics, scraping, and account-connected workflows. Use the Wonda CLI for editing operations such as trim, crop, merge, overlays, captions, audio mixing, and silence removal. CLI editing runs locally with ffmpeg or hyperframes, then uploads the result back to your media library. ## Base URL ``` https://api.wondercat.ai/api/v1 ``` All endpoints are served under this base URL. The current API version is **v1**. ## Request and response format - All JSON request bodies must include `Content-Type: application/json`. - All responses are JSON. - Dates are ISO 8601 strings. - IDs in path parameters are UUIDs unless the endpoint states otherwise. ## Async pattern Generation and publishing operations are asynchronous. When you submit work, the API returns a job ID immediately. Poll the matching job endpoint until the job reaches a terminal status. 1. Submit a request, such as `POST /image/generate` or `POST /publish/instagram`. 2. Poll `GET /jobs/inference/{inferenceJobId}` or `GET /jobs/publish/{outputJobId}` until `status` is `"succeeded"` or `"failed"`. 3. Read output media or publish details from the completed job response. Local CLI editing is different: `wonda edit video`, `wonda edit image`, and `wonda edit audio` render on your machine and return the uploaded `mediaId` directly. The REST editing endpoints are retired and return `410 Gone` with CLI migration hints. ## Main surfaces | Surface | Behavior | Use case | | --------------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | | Direct generation endpoints | Submit a prompt and params, then poll an inference job. Prompts are used as provided. | Automated image, video, text, music, speech, transcription, and analysis workflows. | | Publishing endpoints | Queue a publish job for connected Instagram or TikTok accounts, then poll the publish job. | Single-asset publishing and social distribution automation. | | Wonda CLI editing | Render locally with ffmpeg or hyperframes, then upload the output media. | Deterministic media transforms, captions, overlays, merges, audio edits, and local post-production. | ## Connect to Claude Use the [Connect Wonda to Claude](/docs/connect-claude) guide to add the cloud twin as a custom remote MCP connector. Paste the Wonda connector URL in Claude, grant OAuth in the browser, and use Wonda tools without a local CLI or API key field. --- # Authentication ## Authorization header Include your API key in the `Authorization` header using the Bearer scheme: **HTTP Header** ``` Authorization: Bearer sk_your_api_key_here ``` ## Key format API keys use the prefix `sk_` followed by 64 hexadecimal characters. Example: ``` sk_a1b2c3d4e5f6... (64 hex chars) ``` ## Managing your keys You can create, rotate, and revoke API keys from the [API Keys settings page](https://wonda.sh/api-keys). Keep your keys secret -- do not expose them in client-side code or public repositories. ## Example request **cURL** ```bash curl https://api.wondercat.ai/api/v1/image/generate \ -H "Authorization: Bearer sk_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"prompt": "A sunset over the ocean", "model": "nano-banana-2"}' ``` If the key is missing or invalid the API returns a `401 Unauthorized` response. --- # Quick Start ## 1. Authenticate Every API request uses a bearer token. ```bash export WONDA_API_KEY="sk_your_api_key_here" export WONDA_API_BASE="https://api.wondercat.ai/api/v1" ``` If you are using the CLI for local editing or scripting, install it and sign in: ```bash npm i -g @degausai/wonda wonda auth login wonda auth check ``` ## 2. Generate an image Submit a direct generation request. The response contains an `inferenceJobId`. ```bash curl -X POST "$WONDA_API_BASE/image/generate" \ -H "Authorization: Bearer $WONDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "nano-banana-2", "prompt": "A polished product shot of a ceramic mug on travertine", "params": { "aspectRatio": "1:1", "resolution": "1K" } }' ``` ```json { "inferenceJobId": "019e1b6c-e954-89d3-bcef-6608093508e5" } ``` ## 3. Poll the inference job Poll until the status is `succeeded` or `failed`. A succeeded image job includes output media records. ```bash curl "$WONDA_API_BASE/jobs/inference/019e1b6c-e954-89d3-bcef-6608093508e5" \ -H "Authorization: Bearer $WONDA_API_KEY" ``` ```json { "inferenceJobId": "019e1b6c-e954-89d3-bcef-6608093508e5", "model": "nano-banana-2", "provider": "runware", "type": "image", "prompt": "A polished product shot of a ceramic mug on travertine", "status": "succeeded", "params": { "aspectRatio": "1:1", "resolution": "1K" }, "errorCode": null, "errorMessage": null, "infoMessage": null, "createdAt": "2026-06-12T12:00:00.000Z", "finishedAt": "2026-06-12T12:00:12.000Z", "outputs": [ { "inferenceJobOutputId": "019e1b6f-5e8d-88c2-a0f3-159684318a10", "outputKey": "image", "outputValue": null, "createdAt": "2026-06-12T12:00:12.000Z", "media": { "mediaId": "019e1b70-6f9e-80fd-8c49-690bcbdaf8db", "url": "https://storage.googleapis.com/...", "mimeType": "image/png", "width": 1024, "height": 1024, "durationMs": null, "fps": null } } ], "attachments": [] } ``` Use the output `media.mediaId` as the input to publishing or local CLI editing. ## 4. Publish to Instagram Get an `instagramAccountId` from `GET /instagram/accounts`, then create a publish job. ```bash curl -X POST "$WONDA_API_BASE/publish/instagram" \ -H "Authorization: Bearer $WONDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mediaId": "019e1b70-6f9e-80fd-8c49-690bcbdaf8db", "instagramAccountId": "550e8400-e29b-41d4-a716-446655440000", "caption": "Made with Wonda", "product": "IMAGE" }' ``` ```json { "outputJobId": "019e1b76-74d7-8c7e-a927-6bd34bfaa114", "status": "queued" } ``` Poll the publish job until it reaches a terminal status. ```bash curl "$WONDA_API_BASE/jobs/publish/019e1b76-74d7-8c7e-a927-6bd34bfaa114" \ -H "Authorization: Bearer $WONDA_API_KEY" ``` ## Next steps - Learn how prompts are handled in [Creative vs Verbatim mode](verbatim-mode). - Explore [Image Generation](image-generation), [Video Generation](video-generation), and [Pricing Estimate](pricing-estimate). - Set up production-safe polling with [Job Polling](job-polling). - Use [Publish to Instagram](publish-instagram) or [Publish to TikTok](publish-tiktok) for destination-specific options. - Use [Video Editing](video-editing), [Image Editing](image-editing), and [Audio Editing](audio-editing) for local CLI editing. --- # Creative vs Verbatim Mode ## The two modes | Mode | Prompt handling | Best for | | -------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Creative | Wonda may enhance, rewrite, or expand a prompt before it reaches a model. | Exploration, brainstorming, and end-user prompts that need improvement. | | Verbatim | Your prompt is sent to the model exactly as provided. | Deterministic pipelines, tests, and prompts produced by your own system. | ## Current public API behavior Direct generation endpoints are verbatim. When you call endpoints such as `POST /image/generate`, `POST /video/generate`, `POST /text/generate`, `POST /music/generate`, or `POST /audio/speech`, the `prompt` field is used as provided. No request flag is required to enable verbatim mode on direct endpoints. | Surface | Default mode | How to switch | | --------------------------- | ------------------------------------------------------ | ----------------- | | Direct generation endpoints | Verbatim | No switch needed. | | CLI generation commands | Verbatim for the prompt passed to the direct endpoint. | No switch needed. | ## Recommendation for API consumers If you are building an automated pipeline where prompts are pre-crafted or generated by your own system, use direct generation endpoints. This keeps outputs predictable and makes request logs easier to audit. Use creative prompt rewriting only in product surfaces where you explicitly control that behavior before calling the API. ## Example: direct image generation ```bash curl -X POST https://api.wondercat.ai/api/v1/image/generate \ -H "Authorization: Bearer sk_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{ "model": "nano-banana-2", "prompt": "16:9 image of a golden retriever on a beach, 35mm photo, late afternoon light", "params": { "aspectRatio": "16:9" } }' ``` The model receives the prompt string in the request body. --- # Image Generation `POST /api/v1/image/generate` ## Request Body | Parameter | Type | Required | Description | | ------------------ | -------- | -------- | -------------------------------------- | | model | string | Yes | Model key (see models below) | | prompt | string | No | Text prompt (required for most models) | | params | object | No | Model-specific parameters | | attachmentMediaIds | string[] | No | Positional media attachment IDs | ## Response ```json { "inferenceJobId": "ij_..." } ``` ## Example **POST /api/v1/image/generate** ```json { "model": "nano-banana-2", "prompt": "A polished product shot of a ceramic mug on travertine", "params": { "aspectRatio": "1:1", "resolution": "1K" } } ``` --- ## Models ### nano-banana-2 General-purpose image generation. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------------------------------------- | | aspectRatio | string | No | 9:16, 16:9, 1:1, 4:5, 4:3, 3:4, 3:2, 2:3, 21:9, 8:1, 1:8, 4:1, 1:4, auto (default 9:16) | | resolution | string | No | 1K, 2K, 4K (default 1K) | | styleId | string | No | Optional style UUID to apply | **Attachments:** Up to 14 optional reference images. ### nano-banana-pro Pro-tier image generation. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------- | | aspectRatio | string | No | 9:16, 16:9, 1:1, 4:5, 4:3, 3:4, 3:2, 2:3, 21:9, auto (default 9:16) | | resolution | string | No | 1K, 2K, 4K (default 1K) | | styleId | string | No | Optional style UUID to apply | **Attachments:** Up to 14 optional reference images. ### seedream-4-5 High-quality image generation. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------------- | | aspectRatio | string | No | 1:1, 4:3, 3:4, 16:9, 9:16, 3:2, 2:3, 21:9 (default 16:9) | | resolution | string | No | 2K, 4K (default 2K) | **Attachments:** Up to 14 optional reference images. ### z-image Fast image generation. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------------------- | | aspectRatio | string | No | 1:1, 21:9, 16:9, 4:3, 3:2, 2:3, 3:4, 9:16, 9:21 (default 9:16) | **Attachments:** None. ### gpt-image-1-5 OpenAI image generation. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------------------- | | aspectRatio | string | No | 1:1, 16:9, 9:16 (default 9:16) | | quality | string | No | auto, high, medium, low (default auto) | | background | string | No | auto, transparent, opaque (default auto) | **Attachments:** Up to 16 optional reference images. ### grok-imagine Fast image generation and editing by xAI. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------ | | aspectRatio | string | No | 9:16, 16:9, 1:1, 4:3, 3:4, 3:2, 2:3, 2:1, 1:2, auto (default 9:16) | | resolution | string | No | 1k, 2k (default 1k) | **Attachments:** 1 optional reference image for editing. ### grok-imagine-pro High-quality image generation and editing by xAI. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------ | | aspectRatio | string | No | 9:16, 16:9, 1:1, 4:3, 3:4, 3:2, 2:3, 2:1, 1:2, auto (default 9:16) | | resolution | string | No | 1k, 2k (default 1k) | **Attachments:** 1 optional reference image for editing. ### birefnet-bg-removal Background removal. No prompt needed. | Parameter | Type | Required | Description | | -------------- | ------ | -------- | -------------------------------------------------------------- | | bgRemovalModel | string | No | portrait, general-fast, general-premium (default general-fast) | | quality | string | No | low, high (default low) | **Attachments:** 1 required image (input_image). ### runware-vectorize Image vectorization. No prompt needed. | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------- | | vectorizeModel | string | No | recraft:1@1, picsart:1@1 (default recraft:1@1) | **Attachments:** 1 required image (input_image). --- # Video Generation `POST /api/v1/video/generate` ## Request Body | Parameter | Type | Required | Description | | ------------------ | -------- | -------- | -------------------------------------- | | model | string | Yes | Model key (see models below) | | prompt | string | No | Text prompt (required for most models) | | params | object | No | Model-specific parameters | | attachmentMediaIds | string[] | No | Positional media attachment IDs | ## Response ```json { "inferenceJobId": "ij_..." } ``` ## Example **POST /api/v1/video/generate** ```json { "model": "sora2", "prompt": "A drone shot flying over a misty forest at sunrise", "params": { "aspectRatio": "16:9", "duration": "8" } } ``` --- ## Models ### sora2 Text-to-video generation. Prompt required. | Parameter | Type | Required | Description | | -------------- | ------- | -------- | --------------------------------------------------- | | aspectRatio | string | No | 16:9, 9:16 | | resolution | string | No | 720p only | | duration | string | No | "4", "8", "12", "16", or "20" seconds (default "8") | | omitFirstScene | boolean | No | Omit the first scene from the output | **Attachments:** 1 optional image. ### sora2pro Pro-tier text-to-video. Prompt required. | Parameter | Type | Required | Description | | -------------- | ------- | -------- | --------------------------------------------------- | | aspectRatio | string | No | 16:9, 9:16 | | resolution | string | No | 720p, 1024p, 1080p (default 720p) | | duration | string | No | "4", "8", "12", "16", or "20" seconds (default "8") | | omitFirstScene | boolean | No | Omit the first scene from the output | **Attachments:** 1 optional image. ### veo3_1-fast Fast video generation with fixed 8-second duration. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ----------- | | aspectRatio | string | No | 16:9, 9:16 | | resolution | string | No | 720p, 1080p | **Duration:** Fixed at 8 seconds. **Attachments:** Up to 2 images (first_frame, last_frame). last_frame requires first_frame to be provided. ### kling2_5-pro Kling 2.5 Pro video generation. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | --------------- | | aspectRatio | string | No | 16:9, 9:16, 1:1 | | duration | number | No | 5 or 10 seconds | **Attachments:** Up to 2 optional images. ### kling_2_6_pro Kling 2.6 Pro with automatic image-to-video. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | --------------- | | aspectRatio | string | No | 16:9, 9:16, 1:1 | | duration | number | No | 5 or 10 seconds | **Attachments:** 1 optional image (auto image-to-video when provided). ### kling_2_6_motion_control Motion-controlled video from a reference image and video. Prompt required. | Parameter | Type | Required | Description | | -------------------- | ------- | -------- | ------------------------------------------------ | | characterOrientation | string | No | image, video | | keepOriginalSound | boolean | No | Keep the original audio from the reference video | **Attachments:** 2 required -- reference_image + reference_video. ### kling_3_pro Text-to-video and image-to-video with fine-grained controls. Prompt required. | Parameter | Type | Required | Description | | -------------- | ------- | -------- | --------------------------------------------- | | aspectRatio | string | No | Aspect ratio of the output video | | duration | number | No | 3 to 15 seconds | | generateAudio | boolean | No | Generate audio for the video | | multiPrompt | boolean | No | Enable multi-prompt mode | | negativePrompt | string | No | Negative prompt to guide generation away from | | cfgScale | number | No | Classifier-free guidance scale (0 to 1) | **Attachments:** Optional start_image, end_image, and up to 2 element slots. ### sync-lipsync-v2-pro Lip-sync a video to an audio track. No prompt needed. | Parameter | Type | Required | Description | | -------------------- | ------ | -------- | ------------------------------------- | | syncMode | string | No | cut_off, loop, bounce, silence, remap | | videoDurationSeconds | number | No | 1 to 600 seconds | **Attachments:** 2 required -- video + audio. ### bria-video-background-removal Remove background from video. No prompt or params needed. **Params:** None. **Attachments:** 1 required video. ### veed-video-background-removal VEED-powered video background removal. No prompt needed. | Parameter | Type | Required | Description | | --------------- | ------- | -------- | --------------------------------------------------- | | subjectIsPerson | boolean | No | Whether the subject is a person (optimizes removal) | **Attachments:** 1 required video. ### veed-video-background-removal-fast Fast variant of VEED video background removal. No prompt needed. | Parameter | Type | Required | Description | | --------------- | ------- | -------- | --------------------------------------------------- | | subjectIsPerson | boolean | No | Whether the subject is a person (optimizes removal) | **Attachments:** 1 required video. ### seedance-2 Text-to-video and image-to-video generation. Routes automatically based on image attachment. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------- | | aspectRatio | string | No | 16:9, 9:16, 4:3, 3:4 | | duration | string | No | "5", "10", or "15" (default "5") | | quality | string | No | high (standard), basic (fast) | **Attachments:** 1 optional image (triggers image-to-video mode). ### seedance-2-omni Reference-based video generation. Use @image1, @audio1 in prompt. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------- | | aspectRatio | string | No | 16:9, 9:16, 1:1, 4:3, 3:4, 21:9 | | duration | string | No | "4" to "15" seconds (default "5") | **Attachments:** Up to 3 images and 1 audio clip (all optional). ### seedance-2-video-edit Edit an existing video using text prompts. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | --------------------------- | | aspectRatio | string | No | 16:9, 9:16, 4:3, 3:4 | | quality | string | No | basic, high (default basic) | **Attachments:** 1 required video. ### grok-imagine-video Video generation by xAI. Text-to-video and image-to-video. Prompt required. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------- | | aspectRatio | string | No | 9:16, 16:9, 1:1, 4:3, 3:4, 3:2, 2:3 (default 9:16) | | duration | string | No | 5, 8, 10, or 15 seconds (default 8) | | resolution | string | No | 480p, 720p (default 720p) | **Attachments:** 1 optional start image for image-to-video. ### topaz-video-upscale Upscale video resolution with Topaz. No prompt needed. | Parameter | Type | Required | Description | | ------------- | ---------------- | -------- | ---------------------- | | upscaleFactor | number | No | 1 to 4 | | targetFps | string \| number | No | "original" or 16 to 60 | | codec | string | No | h265, h264 | **Attachments:** 1 required video. --- # Text Generation `POST /api/v1/text/generate` ## Request Body | Parameter | Type | Required | Description | | ------------------ | -------- | -------- | ------------------------------------------------ | | model | string | Yes | Model key (see models below) | | prompt | string | No | Text prompt (required for openrouter-completion) | | params | object | No | Model-specific parameters | | attachmentMediaIds | string[] | No | Positional media attachment IDs | ## Response ```json { "inferenceJobId": "ij_..." } ``` ## Example **POST /api/v1/text/generate** ```json { "model": "openrouter-completion", "prompt": "Write a tagline for a sustainable fashion brand", "params": { "llm": "anthropic/claude-sonnet-4.6", "systemPrompt": "You are a creative copywriter specializing in fashion.", "reasoningEnabled": true, "reasoningEffort": "medium", "maxOutputTokens": 4096 } } ``` --- ## Models ### openrouter-completion LLM text completion via OpenRouter. Prompt required. | Parameter | Type | Required | Description | | ------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | llm | string | No | openai/gpt-5.2, openai/gpt-5.1, openai/gpt-5-chat, google/gemini-3-pro-preview, google/gemini-3-flash-preview, anthropic/claude-opus-4.5, anthropic/claude-sonnet-4.5, anthropic/claude-sonnet-4.6, x-ai/grok-4, x-ai/grok-4.3 (default x-ai/grok-4.3) | | systemPrompt | string | No | System prompt to set the LLM persona and behavior | | reasoningEnabled | boolean | No | Enable reasoning/chain-of-thought mode | | reasoningEffort | string | No | minimal, low, medium, high, xhigh | | reasoningMaxTokens | number | No | 1024 to 32000 | | maxOutputTokens | number | No | 1024 to 128000 | **Attachments:** Up to 4 optional media attachments. #### Available LLMs | Provider | Model | | --------- | ------------------------------- | | OpenAI | `openai/gpt-5.2` | | OpenAI | `openai/gpt-5.1` | | OpenAI | `openai/gpt-5-chat` | | Google | `google/gemini-3-pro-preview` | | Google | `google/gemini-3-flash-preview` | | Anthropic | `anthropic/claude-opus-4.5` | | Anthropic | `anthropic/claude-sonnet-4.5` | | Anthropic | `anthropic/claude-sonnet-4.6` | | xAI | `x-ai/grok-4` | | xAI | `x-ai/grok-4.3` | ### extract-brand-style Extract brand style attributes from images. Prompt is hardcoded internally. **Params:** None. **Attachments:** 1 to 8 brand images (first is required). --- # Music Generation `POST /api/v1/music/generate` ## Request Body | Parameter | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------- | | model | string | Yes | Model key (see models below) | | prompt | string | Yes | Text prompt describing the desired music | | params | object | No | Model-specific parameters | ## Response ```json { "inferenceJobId": "ij_..." } ``` ## Example **POST /api/v1/music/generate** ```json { "model": "suno-music", "prompt": "Upbeat electronic track with a driving bassline", "params": { "instrumental": true } } ``` --- ## Models ### suno-music AI music generation via Suno. Prompt required. | Parameter | Type | Required | Description | | ------------ | ------- | -------- | ----------------------------------------------- | | instrumental | boolean | No | Generate instrumental-only track (default true) | **Attachments:** None. --- # Speech (TTS) `POST /api/v1/audio/speech` ## Request Body | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------ | | model | string | Yes | Model key (see models below) | | prompt | string | Yes | Text to synthesize into speech | | params | object | No | Model-specific parameters | ## Response ```json { "inferenceJobId": "ij_..." } ``` ## Example **POST /api/v1/audio/speech** ```json { "model": "elevenlabs-tts", "prompt": "Welcome to Wonda, the AI-powered video editing platform.", "params": { "voiceId": "voice_abc123", "stability": 0.5, "similarityBoost": 0.75 } } ``` --- ## Models ### elevenlabs-tts Text-to-speech via ElevenLabs. Prompt is the text to synthesize. | Parameter | Type | Required | Description | | --------------- | ------ | -------- | --------------------------- | | voiceId | string | Yes | ElevenLabs voice identifier | | stability | number | No | Voice stability (0 to 1) | | similarityBoost | number | No | Similarity boost (0 to 1) | **Attachments:** None. --- # Transcription (STT) `POST /api/v1/audio/transcribe` ## Request Body | Parameter | Type | Required | Description | | ------------------ | -------- | -------- | ---------------------------- | | model | string | Yes | Model key (see models below) | | params | object | No | Model-specific parameters | | attachmentMediaIds | string[] | Yes | Audio file to transcribe | ## Response ```json { "inferenceJobId": "ij_..." } ``` ## Example **POST /api/v1/audio/transcribe** ```json { "model": "elevenlabs-stt", "params": { "languageCode": "en" }, "attachmentMediaIds": ["media_abc123"] } ``` --- ## Models ### elevenlabs-stt Speech-to-text via ElevenLabs. No prompt needed. | Parameter | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------------------------------- | | languageCode | string | No | BCP-47 language code (e.g. en, es, fr). Auto-detected if omitted. | **Attachments:** 1 required audio file (wav, mp3, mpeg, m4a, ogg). --- # Dialogue `POST /api/v1/audio/dialogue` ## Request Body | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------------- | | model | string | Yes | Model key (see models below) | | params | object | Yes | Model-specific parameters (speakers, script, etc.) | ## Response ```json { "inferenceJobId": "ij_..." } ``` ## Example **POST /api/v1/audio/dialogue** ```json { "model": "elevenlabs-dialogue", "params": { "speakers": [ { "label": "ALICE", "voiceId": "voice_alice123" }, { "label": "BOB", "voiceId": "voice_bob456" } ], "script": "ALICE: Hey Bob, have you tried the new editor?\nBOB: Yes! The AI features are incredible.\nALICE: I know, right? It saves so much time.", "stability": 0.5 } } ``` --- ## Models ### elevenlabs-dialogue Multi-speaker dialogue generation via ElevenLabs. No prompt needed. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------------------------------------------- | | speakers | array | Yes | Array of { label: string, voiceId: string }. Each label must match a speaker in the script. | | script | string | Yes | Dialogue script. Format: SPEAKER_LABEL: text (one line per speaker turn). | | stability | number | No | Voice stability (0 to 1) | **Attachments:** None. #### Script Format ```text ALICE: Hey Bob, have you tried the new editor? BOB: Yes! The AI features are incredible. ALICE: I know, right? It saves so much time. ``` Each line must begin with a speaker label that matches one of the entries in the `speakers` array, followed by a colon and the text to speak. --- # Extract Timestamps `POST /api/v1/alignment/extract-timestamps` ## Request Body | Parameter | Type | Required | Description | | ------------------ | -------- | -------- | ---------------------------- | | model | string | Yes | Model key (see models below) | | params | object | Yes | Model-specific parameters | | attachmentMediaIds | string[] | Yes | Audio file to align against | ## Response ```json { "inferenceJobId": "ij_..." } ``` ## Example **POST /api/v1/alignment/extract-timestamps** ```json { "model": "elevenlabs-extract-timestamps", "params": { "text": "Welcome to Wonda, the AI-powered video editing platform." }, "attachmentMediaIds": ["media_abc123"] } ``` --- ## Models ### elevenlabs-extract-timestamps Align a transcript to audio and extract word-level timestamps via ElevenLabs. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------------- | | text | string | Yes | The transcript text to align against the audio | **Attachments:** 1 required audio file (wav, mp3, mpeg, m4a). --- # Video Editing Video editing is CLI-first. `wonda edit video` downloads inputs when needed, renders locally with ffmpeg or hyperframes, uploads the output media, and prints the resulting `mediaId`. There is no server-side editor job for current video edits. The retired `POST /api/v1/video/edit` endpoint returns `410 Gone` with a migration hint for known operations. Move scripts to the CLI commands below. ## Setup ```bash npm i -g @degausai/wonda wonda auth login wonda doctor ``` `wonda doctor` checks local prerequisites such as ffmpeg. Hyperframes operations such as `textOverlay` and `animatedCaptions` also need the bundled Chromium renderer. ## Usage ```bash wonda edit video --operation \ --media \ --params '{...}' \ --wait -o ./out.mp4 ``` Common flags: | Flag | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------- | | `--operation` | Required operation name. Run `wonda operations list` to see the local registry. | | `--media` | Media IDs or local file paths. Pass multiple inputs as a comma-separated value, such as `--media ,`. | | `--params` | Raw JSON object for operation-specific params. Run `wonda operations info ` for the shape. | | `--preset` | Featured preset name scoped to `--operation`. Explicit `--params` values override preset params. | | `--audio-media` | Audio media ID or local audio file for `editAudio`. | | `--prompt-text` | Required text for `textOverlay`. | | `--caption-segments` | Raw JSON array for `animatedCaptions`. Produce timing data with `wonda alignment extract-timestamps`. | | `--wait` | Accepted for compatibility. Local renders finish before output is printed. | | `-o`, `--output` | Download the rendered output to a local file path. Implies `--wait`. | Video-only shortcut flags: | Flag | Applies to | Description | | ------------------------ | -------------- | ---------------------------------------------------------------------------------------- | | `--threshold` | `splitScenes` | Scene-change sensitivity from `0.01` to `0.9`. | | `--min-clip-duration` | `splitScenes` | Minimum scene duration in seconds. | | `--mode` | `splitScenes` | `split` for separate scene files or `omit` to remove one scene and concatenate the rest. | | `--output-selection` | `splitScenes` | `first`, `last`, or a 1-indexed scene number. | | `--duration-ms` | `imageToVideo` | Hold a still image for this many milliseconds, from `500` to `60000`. | | `--silence-threshold-db` | `skipSilence` | Silence detection noise floor in dB. | | `--min-silence-duration` | `skipSilence` | Shortest gap that counts as silence, in seconds. | | `--max-silence-duration` | `skipSilence` | Silence beyond this gets cut; remaining silence is bounded to this value. | ## Output Without `--output`, a successful local render prints: ```json { "mediaId": "019e1b70-6f9e-80fd-8c49-690bcbdaf8db", "status": "succeeded" } ``` With `--output`, the CLI downloads the result and prints the local path plus `mediaId`: ```json { "path": "./trimmed.mp4", "mediaId": "019e1b70-6f9e-80fd-8c49-690bcbdaf8db" } ``` `splitScenes` in split mode prints a `scenes` array because it can produce multiple media outputs. ## Operations | Operation | Renderer | Inputs | Key params | | ------------------ | ----------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `trim` | ffmpeg | One video | `trimStartMs`, `trimEndMs` | | `crop` | ffmpeg | One video | `aspectRatio`, `customWidth`, `customHeight`, `cropPercent`, `cropAxis`, `position`, `paddingTop` | | `merge` | ffmpeg | Multiple videos | Input order comes from `--media ,,...` | | `overlay` | ffmpeg | Base video plus overlay video | `position`, `resizePercent`, `resizeAxis`, `baseVideoVolume`, `overlayVideoVolume`, `overlayStartMs`, `overlayDurationMs` | | `splitScreen` | ffmpeg | Two videos | `targetAspectRatio` | | `speed` | ffmpeg | One video | `speed`, `preservePitch` | | `volume` | ffmpeg | One video | `volume`, `muted` | | `reverseVideo` | ffmpeg | One video | None | | `extractFrame` | ffmpeg | One video | `timestampMs` or `timestampPercent` | | `extractAudio` | ffmpeg | One video | None | | `editAudio` | ffmpeg | One video plus one audio input | `videoVolume`, `audioVolume`, `audioStartMs`, `audioEndMs`, `audioOffsetMs` | | `imageToVideo` | ffmpeg | One image | `durationMs`, or the `--duration-ms` shortcut | | `skipSilence` | ffmpeg | One video with audio | `silenceThresholdDb`, `minSilenceDuration`, `maxSilenceDuration` | | `splitScenes` | ffmpeg | One video | `mode`, `outputSelection`, `threshold`, `minClipDuration` | | `textOverlay` | hyperframes | One video or image plus `--prompt-text` | Styling params such as `fontFamily`, `position`, `sizePercent`, `fontSizeScale`, `strokeWidth` | | `animatedCaptions` | hyperframes | One video plus `--caption-segments` | Caption styling params such as `fontFamily`, `position`, `sizePercent`, `fontSizeScale`, `strokeWidth`, `highlightColor` | For exact parameter defaults and ranges, run: ```bash wonda operations info trim wonda operations info textOverlay ``` ## Examples Trim a video: ```bash wonda edit video --operation trim --media 019e1b70-6f9e-80fd-8c49-690bcbdaf8db \ --params '{"trimStartMs":3000,"trimEndMs":10000}' \ --wait -o ./trimmed.mp4 ``` Merge videos locally: ```bash wonda edit video --operation merge \ --media 019e1b70-6f9e-80fd-8c49-690bcbdaf8db,019e1b72-0742-8bae-bb93-b569036d05af \ --wait -o ./merged.mp4 ``` Add a static text overlay: ```bash wonda edit video --operation textOverlay --media 019e1b70-6f9e-80fd-8c49-690bcbdaf8db \ --prompt-text "Launch day" \ --params '{"fontFamily":"TikTok Sans SemiCondensed","position":"top-center","sizePercent":66,"fontSizeScale":0.5,"strokeWidth":4.5}' \ --wait -o ./with-text.mp4 ``` Render animated captions: ```bash wonda edit video --operation animatedCaptions --media 019e1b70-6f9e-80fd-8c49-690bcbdaf8db \ --caption-segments '[{"text":"Launch","startS":0},{"text":"day","startS":0.4}]' \ --params '{"fontFamily":"TikTok Sans SemiCondensed","position":"bottom-center","sizePercent":80}' \ --wait -o ./captioned.mp4 ``` Replace or mix audio: ```bash wonda edit video --operation editAudio \ --media 019e1b70-6f9e-80fd-8c49-690bcbdaf8db \ --audio-media 019e1b73-32a4-8c9e-8bbf-3661a14a5716 \ --params '{"videoVolume":30,"audioVolume":80,"audioOffsetMs":2000}' \ --wait -o ./with-audio.mp4 ``` --- # Image Editing Image editing is CLI-first. `wonda edit image` handles exact pixel crops locally, then uploads the result and prints the output `mediaId`. The retired `POST /api/v1/image/edit` endpoint returns `410 Gone` with a CLI migration hint. Move image editing scripts to the commands below. ## Setup ```bash npm i -g @degausai/wonda wonda auth login wonda doctor ``` ## Crop an image Use `imageCrop` for exact pixel rectangles. ```bash wonda edit image --operation imageCrop \ --media 019e1b70-6f9e-80fd-8c49-690bcbdaf8db \ --params '{"cropPixelX":100,"cropPixelY":50,"cropPixelWidth":500,"cropPixelHeight":500}' \ --wait -o ./cropped.png ``` Parameters: | Parameter | Type | Description | | ----------------- | ---- | ------------------------------------------- | | `cropPixelX` | int | Left edge of the crop rectangle, in pixels. | | `cropPixelY` | int | Top edge of the crop rectangle, in pixels. | | `cropPixelWidth` | int | Crop rectangle width, in pixels. | | `cropPixelHeight` | int | Crop rectangle height, in pixels. | Without `--output`, the CLI prints: ```json { "mediaId": "019e1b70-6f9e-80fd-8c49-690bcbdaf8db", "status": "succeeded" } ``` ## Add text to an image `textOverlay` is a local hyperframes operation. Invoke it through `wonda edit video` even when the input media is an image. ```bash wonda edit video --operation textOverlay \ --media 019e1b70-6f9e-80fd-8c49-690bcbdaf8db \ --prompt-text "New drop" \ --params '{"fontFamily":"TikTok Sans SemiCondensed","position":"center","sizePercent":66,"fontSizeScale":0.5,"strokeWidth":4.5}' \ --wait -o ./with-text.png ``` Common styling params include `fontFamily`, `position`, `sizePercent`, `fontSizeScale`, `textColor`, `strokeColor`, `strokeWidth`, `highlightColor`, and `showShadow`. Run `wonda operations info textOverlay` for the current shape. ## Related image workflows AI image editing, background removal, and vectorization are direct generation workflows. Use `POST /image/generate` with the appropriate model and attachments, or use the matching CLI generation commands. --- # Audio Editing Audio editing is CLI-first. `wonda edit audio` runs local ffmpeg operations, uploads the result, and prints the output `mediaId`. The retired `POST /api/v1/audio/edit` endpoint returns `410 Gone` with a CLI migration hint. Move audio trim scripts to the CLI command below. ## Setup ```bash npm i -g @degausai/wonda wonda auth login wonda doctor ``` ## Trim audio Use `audioTrim` with `--audio-media`. The value can be a Wonda media ID or a local file path. ```bash wonda edit audio --operation audioTrim \ --audio-media 019e1b73-32a4-8c9e-8bbf-3661a14a5716 \ --params '{"trimStartMs":1000,"trimEndMs":8000}' \ --wait -o ./trimmed.mp3 ``` Parameters: | Parameter | Type | Description | | ------------- | ---- | ---------------------------------------------------------------- | | `trimStartMs` | int | Start of the kept range, in milliseconds. | | `trimEndMs` | int | End of the kept range, in milliseconds. `0` means until the end. | Without `--output`, the CLI prints: ```json { "mediaId": "019e1b73-32a4-8c9e-8bbf-3661a14a5716", "status": "succeeded" } ``` ## Audio AI operations Audio enhancement and voice extraction are direct inference workflows, not editor operations: | Need | API endpoint | CLI command | | --------------------------- | --------------------------- | -------------------------------------------- | | Denoise or enhance audio | `POST /audio/enhance` | `wonda audio enhance ` | | Isolate vocals from a track | `POST /audio/extract-voice` | `wonda audio extract-voice ` | Both API endpoints expect an audio media input. The CLI commands can accept a video or audio input and handle local audio extraction before calling the API. --- # Instagram ## Single media publish `POST /api/v1/publish/instagram` | Parameter | Type | Required | Description | | -------------------- | ------------- | -------- | --------------------------------------------------------------------------------------- | | `mediaId` | string | Yes | Media ID to publish. | | `instagramAccountId` | string (UUID) | Yes | Connected Instagram account ID from `GET /instagram/accounts`. | | `caption` | string | No | Post caption. | | `altText` | string | No | Alt text for accessibility. | | `product` | string | No | `"IMAGE"`, `"REELS"`, or `"STORIES"`. Images default to IMAGE, videos default to REELS. | | `shareToFeed` | boolean | No | Share a Reel to the main feed as well. | The operation is asynchronous. The initial response returns an `outputJobId` and a `status`. Poll `GET /jobs/publish/{outputJobId}` until the status reaches `succeeded` or `failed`. ```json { "outputJobId": "019e1b76-74d7-8c7e-a927-6bd34bfaa114", "status": "queued" } ``` ## Example ```json { "mediaId": "019e1b70-6f9e-80fd-8c49-690bcbdaf8db", "instagramAccountId": "550e8400-e29b-41d4-a716-446655440000", "caption": "Check out this new video!", "product": "REELS", "shareToFeed": true } ``` ## Instagram carousel `POST /api/v1/publish/instagram/carousel` | Parameter | Type | Required | Description | | -------------------- | ------------- | -------- | ---------------------------------------------------------------- | | `mediaIds` | string[] | Yes | 2 to 10 image media IDs. Instagram carousels only accept images. | | `instagramAccountId` | string (UUID) | Yes | Connected Instagram account ID from `GET /instagram/accounts`. | | `caption` | string | No | Optional caption, up to 2200 characters. | ```json { "mediaIds": [ "019e1b70-6f9e-80fd-8c49-690bcbdaf8db", "019e1b72-0742-8bae-bb93-b569036d05af" ], "instagramAccountId": "550e8400-e29b-41d4-a716-446655440000", "caption": "New carousel" } ``` ```json { "instagramPublishId": "b2d7e2ae-1d3d-4c88-8f01-0d2c8fb2d9b1", "igMediaId": "1789...", "permalink": "https://instagram.com/p/...", "imageCount": 2 } ``` --- # TikTok ## Single video publish `POST /api/v1/publish/tiktok` | Parameter | Type | Required | Description | | -------------------- | ------------- | -------- | ------------------------------------------------------------------------------------- | | `mediaId` | string | Yes | Video media ID to publish. | | `tiktokAccountId` | string (UUID) | Yes | Connected TikTok account ID from `GET /tiktok/accounts`. | | `caption` | string | No | Post caption. | | `privacyLevel` | string | No | `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, or `SELF_ONLY`. | | `isAigc` | boolean | No | Flag content as AI-generated. | | `postMode` | string | No | `"direct"` or `"inbox"`. | | `disableComment` | boolean | No | Disable comments. | | `disableDuet` | boolean | No | Disable duet. | | `disableStitch` | boolean | No | Disable stitch. | | `discloseCommercial` | boolean | No | Disclose commercial content. | | `brandOrganic` | boolean | No | Mark as brand organic content. | | `brandedContent` | boolean | No | Mark as branded content. | The operation is asynchronous. The initial response returns an `outputJobId` and a `status`. Poll `GET /jobs/publish/{outputJobId}` until the status reaches `succeeded` or `failed`. ```json { "outputJobId": "019e1b76-74d7-8c7e-a927-6bd34bfaa114", "status": "queued" } ``` ## Example ```json { "mediaId": "019e1b70-6f9e-80fd-8c49-690bcbdaf8db", "tiktokAccountId": "550e8400-e29b-41d4-a716-446655440000", "caption": "Made with AI #wonda", "privacyLevel": "SELF_ONLY", "isAigc": true, "postMode": "direct" } ``` ## TikTok photo carousel `POST /api/v1/publish/tiktok/carousel` | Parameter | Type | Required | Description | | -------------------- | ------------- | -------- | --------------------------------------------------------------------- | | `mediaIds` | string[] | Yes | 2 to 35 media IDs. TikTok carousels only accept JPEG and WebP images. | | `tiktokAccountId` | string (UUID) | Yes | Connected TikTok account ID from `GET /tiktok/accounts`. | | `caption` | string | No | Optional caption, up to 90 characters. | | `privacyLevel` | string | No | Defaults to `SELF_ONLY`. | | `postMode` | string | No | `direct` or `inbox`. Defaults to `direct`. | | `coverIndex` | number | No | Index of the cover image to use. Defaults to `0`. | | `isAigc` | boolean | No | Defaults to `false`. | | `disableComment` | boolean | No | Disable comments. | | `disableDuet` | boolean | No | Disable duet. | | `disableStitch` | boolean | No | Disable stitch. | | `discloseCommercial` | boolean | No | Defaults to `false`. | | `brandOrganic` | boolean | No | Defaults to `false`. | | `brandedContent` | boolean | No | Defaults to `false`. | TikTok carousel publishing rejects S3-backed media. Use GCS-hosted JPEG or WebP images. ```json { "mediaIds": [ "019e1b70-6f9e-80fd-8c49-690bcbdaf8db", "019e1b72-0742-8bae-bb93-b569036d05af" ], "tiktokAccountId": "550e8400-e29b-41d4-a716-446655440000", "caption": "New carousel", "privacyLevel": "SELF_ONLY", "postMode": "direct", "coverIndex": 0 } ``` ```json { "tiktokPublishId": "c34a4b1f-b2ff-4f3f-a4fb-6cf2ca8f9e2c", "publishId": "v0...", "tiktokMediaId": "v0...", "permalink": "https://www.tiktok.com/@example/video/...", "imageCount": 2 } ``` --- # Distribution Accounts ## What this surface covers This surface is separate from the direct publish endpoints. Use it to discover connected accounts and inspect publish history, especially when you need cursor-based pagination or need to filter previous Instagram and TikTok publishes. ## Instagram accounts `GET /api/v1/instagram/accounts` Returns connected Instagram accounts that are currently active for the authenticated Wonda account. ```json { "accounts": [ { "instagramAccountId": "550e8400-e29b-41d4-a716-446655440000", "igUserId": "17841400000000000", "username": "wonda", "accountType": "BUSINESS", "status": "active", "scopes": ["instagram_basic", "instagram_content_publish"] } ] } ``` ## TikTok accounts `GET /api/v1/tiktok/accounts` Returns connected TikTok accounts that are currently active for the authenticated Wonda account. ```json { "accounts": [ { "tiktokAccountId": "550e8400-e29b-41d4-a716-446655440000", "tiktokUserId": "1234567890", "username": "wonda", "displayName": "Wonda Studio", "avatarUrl": "https://...", "status": "active", "grantedScopes": ["video.publish", "user.info.basic"] } ] } ``` ## Instagram publish history `GET /api/v1/instagram/publishes` | Parameter | Type | Required | Description | | ------------------------ | -------------------------------------------- | -------- | -------------------------------------------------------------------------------- | | limit | number | No | Maximum number of rows to return. Defaults to 10. | | status | queued \| in_progress \| succeeded \| failed | No | Filter by publish status. | | product | IMAGE \| REELS \| STORIES \| CAROUSEL | No | Filter by Instagram product type. | | instagramAccountId | string (UUID) | No | Filter by a specific connected Instagram account. | | cursorCreatedAt | string (datetime) | No | Cursor timestamp for pagination. Must be provided with cursorInstagramPublishId. | | cursorInstagramPublishId | string (UUID) | No | Cursor publish ID for pagination. Must be provided with cursorCreatedAt. | The cursor pair must always travel together. The response returns a `nextCursor` object when more rows are available. ```json { "items": [ { "instagramPublishId": "b2d7e2ae-1d3d-4c88-8f01-0d2c8fb2d9b1", "instagramAccountId": "550e8400-e29b-41d4-a716-446655440000", "product": "CAROUSEL", "status": "succeeded", "sourceUrl": "https://...", "caption": "Spring launch", "altText": null, "containerId": "1789...", "igMediaId": "1789...", "permalink": "https://instagram.com/p/...", "createdAt": "2026-03-19T12:34:56.000Z", "publishedAt": "2026-03-19T12:35:45.000Z" } ], "nextCursor": { "createdAt": "2026-03-19T12:34:56.000Z", "instagramPublishId": "b2d7e2ae-1d3d-4c88-8f01-0d2c8fb2d9b1" } } ``` ## TikTok publish history `GET /api/v1/tiktok/publishes` | Parameter | Type | Required | Description | | --------------------- | -------------------------------------------- | -------- | ----------------------------------------------------------------------------- | | limit | number | No | Maximum number of rows to return. Defaults to 10. | | status | queued \| in_progress \| succeeded \| failed | No | Filter by publish status. | | tiktokAccountId | string (UUID) | No | Filter by a specific connected TikTok account. | | cursorCreatedAt | string (datetime) | No | Cursor timestamp for pagination. Must be provided with cursorTiktokPublishId. | | cursorTiktokPublishId | string (UUID) | No | Cursor publish ID for pagination. Must be provided with cursorCreatedAt. | TikTok history uses the same cursor pattern as Instagram. The response includes `nextCursor` only when more rows are available. ```json { "items": [ { "tiktokPublishId": "c34a4b1f-b2ff-4f3f-a4fb-6cf2ca8f9e2c", "tiktokAccountId": "550e8400-e29b-41d4-a716-446655440000", "sourceUrl": "https://...", "caption": "Launch day", "privacyLevel": "SELF_ONLY", "status": "succeeded", "publishId": "v0...", "tiktokMediaId": "v0...", "isAigc": true, "errorCode": null, "errorMessage": null, "createdAt": "2026-03-19T12:34:56.000Z", "publishedAt": "2026-03-19T12:35:45.000Z" } ], "nextCursor": { "createdAt": "2026-03-19T12:34:56.000Z", "tiktokPublishId": "c34a4b1f-b2ff-4f3f-a4fb-6cf2ca8f9e2c" } } ``` --- # Distribution Publishing Destination-specific publish docs cover single media and carousel publishing. Use this page when you need the carousel response shape, account lookup context, or publish history context. For connected accounts, start with [Distribution accounts](distribution-accounts). ## Instagram carousel publishing `POST /api/v1/publish/instagram/carousel` | Parameter | Type | Required | Description | | ------------------ | ------------- | -------- | ---------------------------------------------------------------- | | mediaIds | string[] | Yes | 2 to 10 image media IDs. Instagram carousels only accept images. | | instagramAccountId | string (UUID) | Yes | Connected Instagram account to publish to. | | caption | string | No | Optional caption, up to 2200 characters. | This endpoint creates a publish record for a carousel post and returns the Instagram publish ID, IG media ID, permalink, and image count. ```json { "instagramPublishId": "b2d7e2ae-1d3d-4c88-8f01-0d2c8fb2d9b1", "igMediaId": "1789...", "permalink": "https://instagram.com/p/...", "imageCount": 4 } ``` Use this endpoint for carousel publishing. The direct Instagram publish endpoint handles a single image or video asset. ## TikTok photo carousel publishing `POST /api/v1/publish/tiktok/carousel` | Parameter | Type | Required | Description | | --------------- | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | mediaIds | string[] | Yes | 2 to 35 media IDs. TikTok carousels only accept JPEG or WebP images. | | tiktokAccountId | string (UUID) | Yes | Connected TikTok account to publish to. | | caption | string | No | Optional caption, up to 90 characters. | | privacyLevel | string | No | PUBLIC_TO_EVERYONE \| MUTUAL_FOLLOW_FRIENDS \| FOLLOWER_OF_CREATOR \| SELF_ONLY. Defaults to SELF_ONLY and is ignored when postMode is inbox. | | postMode | string | No | direct \| inbox. Defaults to direct. | | coverIndex | number | No | Index of the cover image to use. Defaults to 0. | TikTok carousel publishing also rejects S3-backed media. Use GCS-hosted images only, and keep the direct publish endpoint for single-video posts. ```json { "tiktokPublishId": "c34a4b1f-b2ff-4f3f-a4fb-6cf2ca8f9e2c", "publishId": "v0...", "tiktokMediaId": "v0...", "imageCount": 6 } ``` --- # Personas and Sessions ## What a persona is A **persona** is one social account Wonda can act as - a LinkedIn profile, an X handle, a Reddit user, an Instagram account. Every action you run through the API names the persona it should run as. Personas are addressed by a short slug you choose when connecting the account: ```text POST /twin/sessions/{persona}/actions/{platform}/{action} ``` A persona holds the logged-in session for that account. You never send platform credentials to the API - you sign in once through Wonda, and the session lives either on your own machine or on the account's hosted browser. See [Routing](/docs/action-routing) for which one runs a given call. ## Listing your personas ```bash curl https://api.wondercat.ai/api/v1/twin/sessions \ -H "Authorization: Bearer $WONDA_API_KEY" ``` The response lists each persona with the platform it belongs to and whether its session is currently usable. ## Session health A persona's session can lapse - a platform signs you out, requires a re-verification, or flags the account. Actions against a lapsed session fail with `needs_auth` rather than silently doing nothing. To check health without running an action, read it: ```bash curl https://api.wondercat.ai/api/v1/twin/sessions/natty/health \ -H "Authorization: Bearer $WONDA_API_KEY" ``` That returns the twin's lifecycle status and its ban-signal health. Do **not** use `POST /twin/needs-auth` as a probe. Despite the name it is a write: it _flags_ the twin as needing re-authentication and pauses its schedules. Call it when you have established a session is dead, never to ask whether it is. Re-authenticating is deliberately a human step: it happens in a visible browser where the person signs in themselves, including any 2FA. There is no API for submitting a platform password, and there will not be one. See [Controlling the Browser](/docs/browser-control) for opening that window. ## Access and roles An API key acts for the account that owns it. Personas belonging to an organization are reachable when your key's account has an **operator** role on that persona. Without one - including a read-only grant, or a persona that does not exist - the call fails with **403** and code `forbidden`. ## Related - [Running Platform Actions](/docs/platform-actions) - the request and response shape - [Action Reference](/docs/action-reference) - every action, its transport, and its payload - [Routing](/docs/action-routing) - my machine versus the cloud --- # Running Platform Actions ## One endpoint per action Everything Wonda can do on a social platform is an endpoint: ```text POST /twin/sessions/{persona}/actions/{platform}/{action} ``` `platform` is `linkedin`, `x`, `reddit`, or `instagram`. `action` is a verb such as `search`, `comment`, or `feed-engage`. The full list, with payload fields for each, is in the [Action Reference](/docs/action-reference). This is the same surface the Wonda MCP server uses. An MCP client and a plain HTTP client have the same capabilities - MCP is a thin wrapper over these endpoints, not a privileged path. ## A read ```bash curl -X POST \ https://api.wondercat.ai/api/v1/twin/sessions/natty/actions/x/mentions \ -H "Authorization: Bearer $WONDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"count": 20}' ``` The payload is a JSON object whose fields come from the action's row in the [Action Reference](/docs/action-reference). An action that takes no arguments still wants a body - send `{}`. ## A write ```bash curl -X POST \ https://api.wondercat.ai/api/v1/twin/sessions/natty/actions/linkedin/comment \ -H "Authorization: Bearer $WONDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "target": "urn:li:activity:7123456789", "text": "The second point matches what we measured." }' ``` Writes consume a **write slot**. Reads do not. Slots are what the daily caps, cooldowns, and spend limits are counted against, so a read-heavy client is not rate-limited by write policy. Some writes accept `"dryRun": true`, intended to run every check and report what would have happened without touching the platform. Support is per-action, and some actions accept the field without honouring it, so it is not a guaranteed safety net - see [Dry runs](/docs/action-errors). ## Response shape A successful call returns the action's result: ```json { "result": { "...": "action-specific" }, "actionRunId": "run_01J...", "notices": [{ "code": "relay_update_available", "message": "..." }] } ``` `actionRunId` identifies the run for audit. `notices` carries soft out-of-band hints riding a successful result; render each `message` and ignore codes you do not recognise rather than failing. ### 202: the session is warming A control session that is not hot yet answers **202**, not 200: ```json { "status": "warming", "retryAfterMs": 2000 } ``` This is not an error and there is no `error` object. A client that treats any non-200 as failure will drop actions that would have succeeded, so branch on the status code before looking for `error`. For a **read**, wait `retryAfterMs` and send the same request again. For a **write**, do not retry blind. A 202 is also returned when the control session goes stale _after_ the action was dispatched, and the spawned action is not cancelled, so the write may still complete. Confirm the current state before re-sending, exactly as with `deferred` - see [Action Errors](/docs/action-errors). A failure returns a structured error rather than an HTTP-only signal: ```json { "error": { "message": "session needs re-authentication", "code": "needs_auth" } } ``` The `code` is a stable, machine-readable value - branch on it, not on the message. See [Action Errors](/docs/action-errors) for the full taxonomy and what to do about each one. ## Discovering actions at runtime The OpenAPI document describes every action endpoint with its typed request body: ```bash curl https://api.wondercat.ai/api/v1/openapi.json ``` Each action is a distinct operation, so generated clients get real parameter types rather than an opaque blob. ## Related - [Action Reference](/docs/action-reference) - all actions and payloads - [Routing](/docs/action-routing) - where an action actually executes - [Action Errors](/docs/action-errors) - the error taxonomy - [Personas and Sessions](/docs/personas-sessions) - what `{persona}` refers to --- # Routing ## Two places an action can run The same API call can execute in one of two places: - **Your machine** - the Wonda desktop app is running, so the action runs in the Wonda Automation Browser (WAB) on your computer, on your own IP, with the account's cookies staying on your device. - **The cloud twin** - a hosted browser session for that persona, behind dedicated mobile or residential IPs, which can run when your machine is off. You do not pick this per call. The backend routes each action, and an API client gets the same routing an MCP client would. ## Engine policy Routing follows your account's engine policy: | Policy | Behaviour | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `auto` (default) | Prefer your machine when its relay is online; fall back to the cloud twin **if the persona is cloud-capable and the account has the Premium cloud-twin entitlement** | | `my_machine` | Only run on your machine; fail rather than fall back to the cloud | | `cloud` | Always run on the hosted twin, even when your machine is online | `my_machine` is the setting to choose when an account must never be touched from hosted infrastructure. `auto` does not guarantee the work continues when your laptop closes. Cloud fallback needs both a cloud-capable persona and Premium cloud-twin entitlement; without them a local persona returns `relay_offline` or `paid_plan_required` instead of moving to the cloud. ## Transport: cookies or WAB Independently of _where_ an action runs, each action has a **transport** - how it talks to the platform: - `cookies` - a direct authenticated request using the persona's stored session. Fast, cheap, and used for most reads. - `wab` - a real browser page driving the platform's UI. Slower, and required for anything the platform does not expose to a plain request. Every action has a sensible default, listed in the [Action Reference](/docs/action-reference). Override it per call when the action supports both: ```bash curl -X POST \ "https://api.wondercat.ai/api/v1/twin/sessions/natty/actions/x/mentions?via=wab" \ -H "Authorization: Bearer $WONDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` `via` accepts `cookies` or `wab`. Anything else is rejected with `command_not_allowed` and `reason: "invalid_payload"`. Forcing `wab` on a read is occasionally useful when the cookie path is being rate-limited by the platform; forcing `cookies` on an action that genuinely needs a browser will fail. ## Pinning the cloud twin Add `engine=cloud` to run on the hosted twin regardless of the account policy: ```text POST /twin/sessions/{persona}/actions/{platform}/{action}?engine=cloud ``` This exists so a caller that means "run this in the cloud" cannot be silently routed to a live local relay. ## Related - [Running Platform Actions](/docs/platform-actions) - [Controlling the Browser](/docs/browser-control) - driving the WAB directly - [Action Errors](/docs/action-errors) --- # Controlling the Browser ## What the WAB is The Wonda Automation Browser is a real, hardened Chrome window on your machine holding a persona's logged-in session. It launches on demand and runs offscreen by default. These endpoints let an API client surface it, point it somewhere, and see what it is showing - the same controls the MCP `wab_*` tools expose. These act on **your machine** and need the Wonda desktop app running. When it is offline they return a clear error; the cloud twin has its own login flow. ## Commands ```text POST /twin/sessions/{persona}/wab/{command} ``` | Command | Effect | | ------------ | ------------------------------------------------------------------- | | `show` | Bring the window on screen so a person can watch | | `hide` | Return it to running offscreen | | `open` | Navigate it to a URL (requires `target`) | | `screenshot` | Capture what it is currently displaying | | `check` | Report whether the session is still signed in (requires `platform`) | `open` takes a target: ```bash curl -X POST \ https://api.wondercat.ai/api/v1/twin/sessions/natty/wab/open \ -H "Authorization: Bearer $WONDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"target": "https://www.linkedin.com/feed/"}' ``` `open` requires a non-empty `target`; a body with only `platform` is rejected with 400. `platform` is what the `check` command takes. The command set is fixed and the argv is built server-side, then re-validated on the relay - a client cannot smuggle an arbitrary command through. ## Status across every persona Status is account-level, so it has its own endpoint rather than a per-persona one: ```bash curl -X POST https://api.wondercat.ai/api/v1/twin/wab/status \ -H "Authorization: Bearer $WONDA_API_KEY" ``` It lists each persona and whether its browser is running. ## Signing a persona in Sessions lapse. Recovering one is a human step by design: open the platform's login page in a **visible** window and let the person sign in themselves, including 2FA. ```bash # 1. surface the window curl -X POST https://api.wondercat.ai/api/v1/twin/sessions/natty/wab/show \ -H "Authorization: Bearer $WONDA_API_KEY" # 2. navigate it to the platform's login page curl -X POST https://api.wondercat.ai/api/v1/twin/sessions/natty/wab/open \ -H "Authorization: Bearer $WONDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"target": "https://x.com/login"}' # 3. after the person signs in, confirm (platform is required here) curl -X POST https://api.wondercat.ai/api/v1/twin/sessions/natty/wab/check \ -H "Authorization: Bearer $WONDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": "x"}' ``` This flow never handles credentials: the person types them into the browser themselves, and no WAB endpoint accepts a password. A separate credential API does exist - `POST /credentials` takes a `password`, `PATCH /credentials/{id}` can update it, and reads can return it decrypted - so do not tell users that platform passwords never cross the API boundary. Prefer this human-in-the-loop flow where you can: it keeps the secret off your integration entirely. ## Related - [Personas and Sessions](/docs/personas-sessions) - [Routing](/docs/action-routing) - when work runs on your machine at all - [Action Errors](/docs/action-errors) - including the offline-machine case --- # Action Errors ## Shape Failures return a structured error, not just an HTTP status: ```json { "error": { "code": "limit_reached", "message": "daily limit reached for linkedin/comment", "deferUntil": "2026-07-28T00:00:00.000Z", "reason": "limit_hit" } } ``` Branch on `code`. It is stable; `message` is for humans and may change. Two optional fields carry the detail a client needs to act without guessing: - `deferUntil` - an ISO timestamp for quota denials (`throttled`, `limit_reached`, `limit_exhausted`). It is when the quota resets, so a client can **re-arm at that time instead of polling**. - `reason` - the granular cause under the coarse code, such as `weekly_limit_hit`, `signal_cooldown`, `twin_paused`, or `relay_offline`. Branch on `code` first; read `reason` when you want the precise cause. ## HTTP status The status tells you the shape before you parse: | Status | Meaning | | ------ | ---------------------------------------------------------------------------- | | 200 | Action completed; body has `result` | | 202 | Control session is warming; body has `status` and `retryAfterMs`, no `error` | | 400 | Unknown action or invalid payload | | 402 | Insufficient credits | | 403 | Cloud twin requires the Premium plan | | 404 | Twin not found | | 423 | Twin is paused, or blocked by a critical platform signal | | 429 | Action throttled | Only 200 and 202 carry a success body. Everything else carries `error`. Treat 202 as a retry with `retryAfterMs`, not a failure - see [Running Platform Actions](/docs/platform-actions). ## Codes | Code | Meaning | What a client should do | | --------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `throttled` | Pacing kicked in - the action was asked for too soon after the last one | Retry later; respect the cooldown rather than tightening the loop | | `deferred` | Deferred by the gate, OR the response was lost after dispatch | Safe to retry for reads; for writes, check state first (see below) | | `limit_reached` | A daily cap for this action was hit | Stop for the day; retrying will not help | | `limit_exhausted` | The account's write allowance is spent | Raise the allowance or wait for the next period | | `needs_auth` | The persona's session lapsed | Have a person sign in - see [Controlling the Browser](/docs/browser-control) | | `sender_blocked` | The platform blocked this account from the action | Do not retry; the account needs attention | | `command_not_allowed` | The action is not permitted for this persona or role | Check the persona's role and account policy | | `unsupported_channel` | No such platform/action pair | Fix the path; see the [Action Reference](/docs/action-reference) | | `not_found` | The persona does not exist, or your key cannot reach it | Check the slug and the key's access | | `validation_error` | A path parameter is missing (persona, platform, or action) | Fix the URL | Access denial does not come back as `not_found`: a persona your key has no operator grant on returns **403** with code `forbidden`, and so does a persona that does not exist, so the response cannot be used to discover which personas exist on other accounts. A malformed payload or an invalid `via` does NOT return `validation_error`: it returns `command_not_allowed` with `reason: "invalid_payload"`. Branching on `reason` is what separates "you sent the wrong fields" from "this persona may not run this action", since both share the code. Every code above except `validation_error` belongs to the twin taxonomy (`TwinErrorCode`). `validation_error` is the general request-level code, raised before the action is dispatched, so it never carries `deferUntil` or `reason`. ## Retrying Retry `throttled`, `limit_reached`, and `limit_exhausted` **at `deferUntil`**, not before. `deferred` needs more care. It covers two situations: the gate declined to run the action now, and the action was dispatched but its terminal response was lost (`reason` is `action_timeout` or `control_unavailable`). Cancellation on timeout is best effort, so a **write** may already have completed. Retrying blindly can produce a duplicate post, comment, or message. For reads, retry freely; for writes, read the current state on the platform and confirm the action did not land before sending it again. That timestamp is why the field exists: the gate tells you when the quota resets so a client re-arms once rather than probing. Do not retry `sender_blocked`, `command_not_allowed`, `unsupported_channel`, `not_found`, or `validation_error`. The outcome will not change, and repeatedly hitting a blocked account makes its standing on the platform worse. If `deferUntil` is absent, back off rather than retrying immediately. Wonda already paces actions server-side; a client that retries tightly fights that pacing instead of benefiting from it. ## Run history `GET /twin/runs` lists control and scheduled `twin_run` records. It is **not** an index of individual action executions: local relay actions create no `twin_run` at all, and cloud action correlation IDs are not represented, so it cannot confirm whether a specific timed-out write landed. Use it as an audit trail, not as confirmation: ```bash curl "https://api.wondercat.ai/api/v1/twin/runs?persona=natty&limit=20" \ -H "Authorization: Bearer $WONDA_API_KEY" ``` Both query parameters are optional. When a run failed, its diagnostic artifacts are available as presigned download URLs: ```bash curl https://api.wondercat.ai/api/v1/twin/runs/{runId}/artifact \ -H "Authorization: Bearer $WONDA_API_KEY" ``` ## Dry runs Some writes accept `"dryRun": true`, intended to run every check - access, allowance, caps, account health - and report what would have happened without touching the platform. **Do not rely on it as a safety net on the hosted path.** Several actions accept the field but do not forward it to the underlying command, so the write is performed for real. `linkedin/send-message` and `linkedin/comment` both behave this way today; `reddit/chat-send` honours it. Until that is fixed, treat `dryRun` as best effort and test against an account you are willing to post from. The [Action Reference](/docs/action-reference) lists `dryRun?` in the payload of the actions that accept the field, which is not the same as the actions that honour it. ## Related - [Running Platform Actions](/docs/platform-actions) - [Action Reference](/docs/action-reference) - [Error Codes](/docs/errors) - the general API error taxonomy --- # Action Reference Every action below is an endpoint: ```text POST /twin/sessions/{persona}/actions/{platform}/{action} ``` Send the payload fields as a JSON object. Fields marked `?` are optional; an empty payload is `{}`. See [Running Platform Actions](/docs/platform-actions) for the request and response shape, and [Routing](/docs/action-routing) for what `transport` means and when you can override it. **Kind** is `read` or `write`. Writes consume a write slot and are subject to your account's allowance, daily caps, and cooldowns; reads are not. Some actions enforce cross-field rules the table cannot show. A row whose fields all read optional may still reject an otherwise reasonable payload, so check the 400 message. Known cases: | Action | Rule | | --- | --- | | `linkedin/activity`, `linkedin/enrich` | exactly one of `target` or `targets` | | `*/feed-engage` | exactly one of `authors` or `keywords` (Instagram requires `authors`) | | `reddit/submit` | exactly one of `text`, `url`, or `mediaRefs` | | `linkedin/salesnav-save-search` | keywords or a facet | ## LinkedIn | Action | Kind | Transport | Payload | | --- | --- | --- | --- | | `activity` | read | `wab` | `target?`, `type?`, `count?`, `targets?` | | `analytics` | read | `cookies` | `target` | | `comment-reactors` | read | `cookies` | `commentUrn`, `count?`, `all?` | | `comment` | write | `wab` | `target`, `text`, `dryRun?` | | `comments` | read | `cookies` | `target`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `company` | read | `cookies` | `target` | | `connect` | write | `wab` | `target`, `message?` | | `connection-status` | read | `cookies` | `targets` | | `connections` | read | `cookies` | none | | `conversations` | read | `cookies` | none | | `delete-comment` | write | `wab` | `target` | | `delete-post` | write | `wab` | `target` | | `edit-comment` | write | `wab` | `target`, `commentId`, `text`, `dryRun?` | | `edit-post` | write | `wab` | `target`, `text` | | `engage-commenters` | write | `wab` | `post`, `actions?`, `replyText?`, `connectNote?`, `maxCommenters?`, `durationMs?`, `dryRun?` | | `enrich-engagers` | read | `cookies` | `activityId`, `reactions?`, `comments?`, `maxProfiles?`, `companyDetail?`, `profileSource?` | | `enrich` | read | `cookies` | `target?`, `via?`, `targets?` | | `feed-engage` | write | `wab` | `authors?`, `keywords?`, `subreddits?`, `reply?`, `replyStyle?`, `personaReply?`, `relevanceThreshold?`, `relevancePrompt?`, `maxReply?`, `perDayCap?`, `maxScan?`, `dryRun?`, `durationMs?`, `maxEngage`, `reactions?`, `expandPosts?`, `engageComments?`, `engageCommentsFrom?`, `maxCommentEngage?`, `navigate?`, `scrollMode?`, `scanIntervalMs?` | | `follow` | write | `wab` | `target` | | `inmail-credits` | read | `cookies` | none | | `inmail` | write | `wab` | `target`, `subject`, `message`, `yesConsumeCredit?`, `dryRun?` | | `invitations` | read | `cookies` | none | | `like` | write | `wab` | `target`, `reaction?`, `comment?` | | `me` | read | `cookies` | none | | `messages` | read | `cookies` | `target` | | `mute` | write | `wab` | `target` | | `notifications` | read | `cookies` | none | | `post-details` | read | `cookies` | `target` | | `post` | write | `wab` | `text`, `visibility?`, `mediaRefs?` | | `posts` | read | `cookies` | `target`, `count?`, `comments?` | | `profile` | read | `cookies` | `target` | | `react` | write | `wab` | `activityId`, `reactionType?` | | `reactions` | read | `cookies` | `target`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `reply-comment` | write | `wab` | `target`, `commentId`, `text`, `dryRun?` | | `salesnav-alerts` | read | `cookies` | none | | `salesnav-connect` | write | `wab` | `urn`, `expectName`, `note?`, `send?` | | `salesnav-create-list` | write | `wab` | `name`, `description?` | | `salesnav-delete-list` | write | `wab` | `id` | | `salesnav-delete-saved-search` | write | `wab` | `id` | | `salesnav-facets` | read | `cookies` | `type?`, `query?` | | `salesnav-insights` | read | `cookies` | `urn` | | `salesnav-list-add` | write | `wab` | `listId`, `urn` | | `salesnav-list-remove` | write | `wab` | `listId`, `urn` | | `salesnav-lists` | read | `wab` | none | | `salesnav-message` | write | `wab` | `urn`, `text`, `expectName`, `subject?`, `send?` | | `salesnav-notifications` | read | `cookies` | `count?` | | `salesnav-personas` | read | `cookies` | none | | `salesnav-profile` | read | `wab` | `urns` | | `salesnav-recent` | read | `cookies` | none | | `salesnav-recommended-companies` | read | `cookies` | `count?`, `all?`, `maxPages?`, `delayMs?` | | `salesnav-recommended-leads` | read | `cookies` | `count?`, `all?`, `maxPages?`, `delayMs?` | | `salesnav-save-lead` | write | `wab` | `urn`, `unsave?` | | `salesnav-save-search` | write | `wab` | `name`, `keywords?`, `seniority?`, `region?`, `industry?`, `company?`, `function?`, `connectionOf?`, `title?`, `pastTitle?`, `pastCompany?`, `school?`, `yearsOfExperience?` | | `salesnav-saved-searches` | read | `cookies` | none | | `salesnav-search` | read | `wab` | `keywords?`, `seniority?`, `region?`, `industry?`, `company?`, `function?`, `connectionOf?`, `title?`, `pastTitle?`, `pastCompany?`, `school?`, `yearsOfExperience?`, `count?`, `maxPages?`, `delayMs?` | | `salesnav-spotlights` | read | `cookies` | `list?`, `limit?`, `changedWithinDays?`, `postedWithinDays?` | | `salesnav-typeahead` | read | `cookies` | `query` | | `salesnav-warm-intro` | read | `cookies` | `urn`, `count?` | | `saves` | read | `cookies` | none | | `search-posts` | read | `wab` | `query`, `maxPosts?`, `minReactions?`, `excludeCompanyPages?`, `sort?`, `dateRange?`, `withAuthorProfile?` | | `search` | read | `cookies` | `query`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `send-message` | write | `wab` | `target`, `message`, `participantName?`, `dryRun?` | | `sent-invitations` | read | `cookies` | none | | `unlike` | write | `wab` | `target`, `reaction?`, `comment?` | | `visit` | write | `wab` | `target`, `dwellMs?`, `noScroll?` | ## X | Action | Kind | Transport | Payload | | --- | --- | --- | --- | | `analytics` | read | `cookies` | `tweetId`, `count?`, `cursor?` | | `bookmark` | write | `wab` | `tweetId` | | `bookmarks` | read | `cookies` | none | | `delete` | write | `wab` | `tweetId` | | `dm-inbox` | read | `cookies` | `count?` | | `dm-read` | read | `cookies` | `conversationId`, `count?` | | `dm-requests` | read | `cookies` | `count?` | | `feed-engage` | write | `wab` | `authors?`, `keywords?`, `subreddits?`, `reply?`, `replyStyle?`, `personaReply?`, `relevanceThreshold?`, `relevancePrompt?`, `maxReply?`, `perDayCap?`, `maxScan?`, `dryRun?`, `durationMs?`, `maxEngage`, `reactions?`, `expandPosts?`, `engageComments?`, `engageCommentsFrom?`, `maxCommentEngage?`, `navigate?`, `scrollMode?`, `scanIntervalMs?` | | `follow` | write | `wab` | `handle` | | `followers` | read | `cookies` | `target`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `following` | read | `cookies` | `target`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `home` | read | `cookies` | `following?` | | `like` | write | `wab` | `tweetId` | | `likes` | read | `cookies` | `target?`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `list-timeline` | read | `cookies` | `target`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `lists` | read | `cookies` | `handle?`, `memberOf?` | | `mentions` | read | `cookies` | `count?` | | `news` | read | `cookies` | `tab?`, `count?` | | `quote` | write | `wab` | `tweetId`, `text`, `mediaRefs?` | | `read` | read | `cookies` | `tweetId`, `count?`, `cursor?` | | `replies` | read | `cookies` | `tweetId`, `count?`, `cursor?` | | `reply` | write | `wab` | `tweetId`, `text`, `mediaRefs?` | | `retweet` | write | `wab` | `tweetId` | | `search` | read | `cookies` | `query`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `thread` | read | `cookies` | `tweetId`, `count?`, `cursor?` | | `tweet` | write | `wab` | `text`, `mediaRefs?` | | `unbookmark` | write | `wab` | `tweetId` | | `unfollow` | write | `wab` | `handle` | | `unlike` | write | `wab` | `tweetId` | | `unretweet` | write | `wab` | `tweetId` | | `user-tweets` | read | `cookies` | `target`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `user` | read | `cookies` | `target` | ## Reddit | Action | Kind | Transport | Payload | | --- | --- | --- | --- | | `analytics` | read | `cookies` | `target` | | `chat-accept-all` | write | `wab` | `delayMs?` | | `chat-accept` | write | `wab` | `conversationId` | | `chat-inbox` | read | `wab` | `count?` | | `chat-messages` | read | `wab` | `conversationId`, `count?` | | `chat-send` | write | `wab` | `conversationId`, `text`, `dryRun?` | | `chat-start` | write | `wab` | `username`, `text`, `dryRun?` | | `comment` | write | `wab` | `parentFullname`, `text`, `postId?`, `dryRun?` | | `comments` | read | `cookies` | `subreddit`, `count?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `delete` | write | `wab` | `fullname`, `postId?` | | `feed-engage` | write | `wab` | `authors?`, `keywords?`, `subreddits?`, `reply?`, `replyStyle?`, `personaReply?`, `relevanceThreshold?`, `relevancePrompt?`, `maxReply?`, `perDayCap?`, `maxScan?`, `dryRun?`, `durationMs?`, `maxEngage`, `reactions?`, `expandPosts?`, `engageComments?`, `engageCommentsFrom?`, `maxCommentEngage?`, `navigate?`, `scrollMode?`, `scanIntervalMs?` | | `feed` | read | `cookies` | `subreddit`, `count?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `home` | read | `cookies` | `target?`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `inbox` | read | `cookies` | `count?`, `after?`, `type?`, `unread?` | | `post` | read | `cookies` | `target`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `rules` | read | `cookies` | `target` | | `save` | write | `wab` | `fullname`, `postId?` | | `saved` | read | `cookies` | `target?`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `search` | read | `cookies` | `query`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `submit` | write | `wab` | `subreddit`, `title`, `text?`, `url?`, `mediaRefs?`, `flair?`, `dryRun?` | | `subreddit` | read | `cookies` | `target` | | `subscribe` | write | `wab` | `target` | | `trending` | read | `cookies` | `target?`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `unsave` | write | `wab` | `fullname`, `postId?` | | `user-comments` | read | `cookies` | `target`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `user-posts` | read | `cookies` | `target`, `count?`, `cursor?`, `after?`, `sort?`, `time?`, `all?`, `maxPages?`, `delayMs?` | | `user` | read | `cookies` | `target` | | `vote` | write | `wab` | `fullname`, `vote`, `postId?` | | `whoami` | read | `cookies` | none | ## Instagram | Action | Kind | Transport | Payload | | --- | --- | --- | --- | | `comment` | write | `wab` | `media`, `text` | | `comments` | read | `cookies` | `target` | | `feed-engage` | write | `wab` | `authors?`, `keywords?`, `subreddits?`, `reply?`, `replyStyle?`, `personaReply?`, `relevanceThreshold?`, `relevancePrompt?`, `maxReply?`, `perDayCap?`, `maxScan?`, `dryRun?`, `durationMs?`, `maxEngage`, `reactions?`, `expandPosts?`, `engageComments?`, `engageCommentsFrom?`, `maxCommentEngage?`, `navigate?`, `scrollMode?`, `scanIntervalMs?` | | `saved` | read | `cookies` | none | ## Actions that only run locally A few verbs are deliberately absent from the API because they cannot run on hosted infrastructure: | Action | Why | | --- | --- | | `x/dm-send` | WAB-only; may also need an encrypted XChat passcode | | `x/dm-accept` | WAB-only; may also need an encrypted XChat passcode | | `x/dm-start` | WAB-only; may also need an encrypted XChat passcode | ### Nested verbs need `?engine=cloud` while your relay is online Actions whose command nests a sub-verb - `x/dm-*`, `reddit/chat-*`, and `linkedin/salesnav-*` - are rejected by the local relay's argv check, which requires the second command token to equal the action name. There is **no automatic fallback** from that. The API only reroutes to the hosted twin when the relay is *lost*; a rejection is an error response, so the call fails outright. With an online relay under the default `auto` policy, these actions therefore require an explicit pin: ```text POST /twin/sessions/{persona}/actions/x/dm-inbox?engine=cloud ``` Without the pin they fail while your machine is online and succeed while it is off, which is a confusing failure to debug. A relay carrying the argv fix removes the need for the pin, but only once that build is installed. These run through the local CLI or the local MCP server instead. The passcode is set with `wonda x dm passcode set`, which is terminal-only by design - the secret never leaves your machine, so there is nothing for the API to forward. It is loaded only when X actually presents its encrypted-XChat gate, so an account without that gate can send DMs without one; the writes stay local because the transport is WAB, not because every account needs a passcode. The DM **reads** (`x/dm-inbox`, `x/dm-read`, `x/dm-requests`) are available over the API like any other read. --- # Connect to Claude ## What you get The Wonda cloud twin lets Claude use your server-side social personas through Wonda tools. Claude can read search results, feeds, profiles, analytics, inboxes, and account health across LinkedIn, Reddit, X, and Instagram. It can also ask Wonda to take approved actions such as posting, replying, commenting, connecting, messaging, scheduling, and running bounded campaigns. This is the cloud connector path. You do not need the Wonda CLI, a local browser, or an API key field in Claude. ## Connect on Claude.ai web or Cowork Add Wonda as a custom connector by URL: ```text https://api.wondercat.ai/mcp ``` Existing connections made with the previous Cloud Run URL keep working; no reconnection is needed. In Claude, open **Customize**, then **Connectors** (or go straight to [claude.ai/customize/connectors](https://claude.ai/customize/connectors)). Click **+**, choose **Add custom connector**, name it `Wonda`, paste the URL, and continue. Claude opens Wonda's OAuth flow in the browser. Sign in with Wonda, choose the account or organization context you want Claude to use, review the consent screen, and approve access. Claude receives OAuth tokens for the Wonda MCP resource at `https://api.wondercat.ai/.well-known/oauth-protected-resource/mcp`. Your Wonda API key stays inside Wonda and is not shown to Claude or copied into the connector form. This custom connector path does not require a Connectors Directory listing or Anthropic review. The Directory path is optional and skipped by default. ## Connect on Claude Desktop Claude Desktop can use the same remote connector when your Claude account has custom connector support. Add Wonda in Claude's connector settings, paste the same connector URL, and complete the OAuth grant in the browser. Technical users can also run Wonda locally: - Use the stdio MCP package with `npx -y @degausai/wonda-mcp` and your normal Wonda CLI authentication. - Use the local `wonda.mcpb` Desktop Extension on macOS when you want platform actions to run through your on-device Wonda Automation Browser and local cookie store. The local paths work in Claude Desktop, Claude Code, and Claude Cowork: the desktop app runs local MCP servers on your machine, outside the Cowork sandbox, so local mode works in Cowork sessions too. Claude web cannot load local servers and needs the remote connector. ## Team and Enterprise For Claude Team and Enterprise workspaces, an organization owner or connector admin may need to add and approve Wonda before members can use it. Add Wonda once as a custom connector, complete the Wonda OAuth grant, and approve it for the workspace. In Wonda, paid organization seats use the organization's twin access and billing context when the OAuth grant is made from that organization. Members still act through their assigned Wonda permissions, spend limits, and platform account connections. ## Consent and approvals Wonda labels read tools as safe for Claude's **Always allow** mode. Read tools have zero write slots and cover retrieval such as feeds, profiles, inboxes, analytics, job status, twin health, and action allowance. Write tools are labeled as needing approval. They request one or more server-side write slots depending on the action. Examples include comments, replies, direct messages, connection requests, follows, saves, votes, posts, schedules, and campaign runs. Claude's consent setting is a client-side prompt. Wonda still enforces the server-side action gate, write-slot allowance, daily caps, cooldowns, account status, billing, and safety checks before anything is sent to a platform. ## Autopilot or supervised mode For supervised use, leave write tools in an ask-first mode. Claude will ask before it asks Wonda to act. For hands-off workflows, allow a bounded campaign or schedule tool instead of approving each small action. A campaign approval lets Wonda run the requested loop server-side within the configured caps, cooldowns, and relevance checks. Setting a write tool to **Always allow** does not mean unlimited action volume. Wonda still applies action allowances, spend limits, daily caps, cooldowns, paused-session checks, and platform health gates. ## Onboard your platform accounts The connector can only act through platform accounts that are already connected to the selected Wonda twin. If a platform session expires, Wonda will ask you to refresh it through the streamed login view before read or write tools continue. ## Troubleshooting - **Claude asks you to connect again**: complete the OAuth grant again from Claude's connector settings. - **A platform account needs auth**: open the Wonda streamed login view for that twin and platform, then sign in. - **402 responses**: add credits or update billing. - **423 responses**: resume the paused twin or clear the account safety state in Wonda. - **429 responses**: wait for the action allowance, platform cooldown, or rate limit window to reset. ## Connectors Directory The Connectors Directory is not required for this connector. Wonda ships as a custom connector by URL, so users can connect without a Directory listing and without Anthropic contact. If Wonda later chooses to request a Directory listing, that should be a separate go or hold decision after live connector verification has passed. The reviewer account must be owned by Wonda, must sit in a Team or Enterprise organization, and must have only Wonda-owned platform identities connected. Do not submit a Directory packet until the connector URL, OAuth metadata, redirect callbacks, read tools, write approvals, and reviewer credentials have all been verified. --- # Connect to Codex ## You may not need this Codex is the one assistant with a second way in: it runs in a terminal, so it can drive Wonda by shelling out to the `wonda` binary directly, the way Claude Code does. Sign in once: ```bash wonda auth login ``` After that Codex can use Wonda with no connector at all. Adding the MCP server below is still worth doing if you want Codex to drive Wonda on a **different computer** than the one you are working on. ## Add the server In Codex, open **Plugins**, then **MCPs**, and click **+ Add Server**. Name it `Wonda`, set **Type** to `Streamable HTTP`, and paste this into **URL**, leaving every other field empty: ```text https://api.wondercat.ai/mcp ``` Save, then sign in to Wonda and approve access. Done once, works forever. ## Prefer the terminal? Add this to `~/.codex/config.toml`: ```toml [mcp_servers.wonda] url = "https://api.wondercat.ai/mcp" oauth_resource = "https://api.wondercat.ai/mcp" ``` Replace any existing `[mcp_servers.wonda]` block rather than pasting a second copy - Codex rejects the whole file if the key appears twice. Then grant Codex access to the server: ```bash codex mcp login wonda ``` That is a different thing from `wonda auth login` above: `wonda auth login` authenticates the CLI, while `codex mcp login` completes the OAuth grant for the MCP server you just configured. ## Verify Ask Codex: ```text Use Wonda to tell me which personas I have. ``` If it lists your personas, the connection works. If it reports none, connect a persona first - see [Personas and Sessions](/docs/personas-sessions). ## Approvals Read tools are safe to allow broadly: they have zero write slots and only retrieve data. Write tools request server-side write slots and should stay ask-first unless you deliberately want a hands-off loop. Codex's approval setting is a client-side prompt. Wonda still enforces the action gate, write-slot allowance, daily caps, cooldowns, account status, and safety checks server-side, so allowing a tool in Codex does not raise those limits. ## Prefer plain HTTP? Codex can also call the Wonda API directly with an API key, without MCP. The tools are a convenience wrapper over the same endpoints - see [Running Platform Actions](/docs/platform-actions). ## Related - [Connect to Claude](/docs/connect-claude) - [Connect to ChatGPT](/docs/connect-chatgpt) - [Running Platform Actions](/docs/platform-actions) --- # Connect to ChatGPT ## What you get ChatGPT gets Wonda's cloud twin: reading feeds, search results, profiles, analytics, and inboxes across LinkedIn, X, Reddit, and Instagram, plus approved writes such as posting, commenting, connecting, messaging, and running bounded campaigns. You do not need the Wonda CLI, a local browser, or an API key pasted into ChatGPT. ## Turn on Developer mode first Open **Settings**, then **Security and login**, and enable **Developer mode**. Developer mode and full MCP support are a **Business and Enterprise/Edu** capability on ChatGPT web, enabled by a workspace admin (Enterprise/Edu can scope it per user). Consumer **Plus and Pro do not have it**, so if you are on one of those plans you cannot add a server here - use [Claude](/docs/connect-claude) or [Codex](/docs/connect-codex), or call the [API directly](/docs/platform-actions). Do this before anything else: the Plugins page has no way to add a server until Developer mode is on. ## Add the server Go to [chatgpt.com/plugins](https://chatgpt.com/plugins) and: 1. Click **+** and name it `Wonda`. 2. Set **Connection** to `Server URL` and paste this into the URL field: ```text https://api.wondercat.ai/mcp ``` 3. Leave **Authentication** on `OAuth`, tick **I understand and want to continue**, and create it. 4. ChatGPT asks you to sign in to Wonda and approve access. Done once, works forever. Your Wonda API key stays inside Wonda and is never shown to ChatGPT. The grant is OAuth against the Wonda MCP resource at `https://api.wondercat.ai/.well-known/oauth-protected-resource/mcp`. ## Verify Ask ChatGPT: ```text Use Wonda to tell me which personas I have. ``` If it lists your personas, the connector works. If it reports none, connect a persona first - see [Personas and Sessions](/docs/personas-sessions). ## Where actions run ChatGPT cannot load a local MCP server, so the connector itself is remote. That does **not** mean every action runs in the cloud: the remote server calls the same twin action endpoints as everything else, and those follow your account's engine policy. Under the default `auto`, an action runs in the Wonda Automation Browser on your machine whenever the Wonda app is online, and only falls back to the hosted twin otherwise. If you need a persona's writes to come from the hosted residential or mobile IP rather than your own connection, set the engine policy to `cloud` - see [Routing](/docs/action-routing). Assuming "remote connector" means "hosted IP" is exactly the mistake worth avoiding. ## Approvals Read tools are labelled safe to allow broadly. Write tools request server-side write slots and should stay ask-first unless you want a hands-off loop. The approval setting in ChatGPT is a client-side prompt. Wonda still enforces the action gate, write-slot allowance, daily caps, cooldowns, account status, and safety checks server-side, so allowing a tool does not raise those limits. ## Team and Enterprise An admin enables Developer mode and may need to approve the server before members can use it. Add Wonda once, complete the OAuth grant, and approve it for the workspace. The OAuth grant binds your Wonda **account**, not an organization: the consent screen has no organization chooser, and the connector does not send the `X-Wonda-Org` header that selects an organization seat, entitlement, or wallet. Actions therefore run against your personal context. Use the CLI or the web app when work must be billed to an organization. ## Related - [Connect to Claude](/docs/connect-claude) - [Connect to Codex](/docs/connect-codex) - [Running Platform Actions](/docs/platform-actions) --- # Upload Media `POST /api/v1/media/upload` ## Content-Type Requests must use `multipart/form-data`. For a single file, use the field name `file`. For multiple files, use any field name per file. ## Supported Formats | Category | MIME Types | | -------- | ----------------------------------- | | Images | jpeg, png, gif, webp, svg+xml | | Videos | mp4, webm, quicktime, x-msvideo | | Audio | mpeg, mp3, wav, ogg, webm, aac, m4a | ## Single-File Response ```json { "mediaId": "uuid", "url": "https://...", "mimeType": "video/mp4", "uploads": { "file": { "mediaId": "uuid", "url": "...", "mimeType": "..." } } } ``` ## Multi-File Response When uploading multiple files, the response includes an `uploads` map keyed by field name, plus an `errors` map for any files that failed. Partial success is possible -- some files may upload successfully while others fail. ```json { "uploads": { "background": { "mediaId": "...", "url": "...", "mimeType": "..." }, "overlay": { "mediaId": "...", "url": "...", "mimeType": "..." } }, "errors": { "badfile": "Unsupported MIME type" } } ``` ## Example **cURL -- single file upload** ```bash curl https://api.wondercat.ai/api/v1/media/upload \ -H "Authorization: Bearer sk_your_api_key_here" \ -F "file=@/path/to/video.mp4" ``` **cURL -- multi-file upload** ```bash curl https://api.wondercat.ai/api/v1/media/upload \ -H "Authorization: Bearer sk_your_api_key_here" \ -F "background=@/path/to/bg.png" \ -F "overlay=@/path/to/overlay.png" ``` ## Request Fields | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------------------------------------------------- | | file | binary | Yes | The file to upload. Use field name "file" for single uploads, or any name for multi-file uploads. | --- # Analytics ## Instagram analytics `GET /api/v1/analytics/instagram` | Parameter | Type | Required | Description | | ------------------ | ------------- | -------- | ----------------------------------------------------------------------------------------------- | | instagramAccountId | string (UUID) | No | Optional when exactly one Instagram account is connected; required when multiple are connected. | The response includes the connected account summary, recent media, and audience insights when available. **Response shape** ```json { "dataLevel": "full", "account": { "username": "wonda", "followerCount": 12000, "mediaCount": 84 }, "accountMetrics": { "reach": 54000, "views": 92000, "followsAndUnfollows": 126, "period": "last_30d" }, "audienceInsights": { "followerDemographics": {}, "reachedAudienceDemographics": {}, "onlineFollowers": {} }, "recentMedia": [] } ``` `accountMetrics` and `audienceInsights` are nullable — they are `null` when the account has `"dataLevel": "basic"`. ## TikTok analytics `GET /api/v1/analytics/tiktok` | Parameter | Type | Required | Description | | --------------- | ------------- | -------- | -------------------------------------------------------------------------------------------- | | tiktokAccountId | string (UUID) | No | Optional when exactly one TikTok account is connected; required when multiple are connected. | The response includes profile stats and a list of recent videos with view and engagement counts. **Response shape** ```json { "account": { "displayName": "Wonda", "followerCount": 12000, "followingCount": 42, "likesCount": 181000, "videoCount": 84, "bio": "Creative automation", "isVerified": false }, "recentVideos": [] } ``` ## Meta Ads analytics `GET /api/v1/analytics/meta-ads` | Parameter | Type | Required | Description | | --------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | metaAdAccountId | string (UUID) | No | Optional selector for one connected Meta ad account. If omitted, a single connected account is used directly or multiple accounts are aggregated. | | objectId | string | No | Overrides the target Ad Library object id. Defaults to the connected ad account when omitted. | | datePreset | string | No | One of the supported date presets. Defaults to last_7d. | | fields | string | No | Comma-separated Meta Insights fields. Defaults to the standard engagement and spend fields. | | breakdowns | string | No | Comma-separated breakdown values. Invalid combinations return a 400 response. | **Example** ```bash curl "https://api.wondercat.ai/api/v1/analytics/meta-ads?datePreset=last_7d" \ -H "Authorization: Bearer YOUR_API_KEY" ``` When one ad account is connected, the endpoint returns a single account payload. When multiple accounts are connected, the response contains per-account rows plus an aggregate summary. **Single-account response** ```json { "accountName": "Wonda Ads", "accountId": "act_123", "datePreset": "last_7d", "summary": { "impressions": 12000, "clicks": 340, "spend": 92.13, "reach": 8300, "cpm": 7.67, "cpc": 0.27, "ctr": 2.83 }, "actions": {}, "rows": [], "deltas": { "spend": 12.5, "impressions": -3.2, "clicks": 8.1, "ctr": 1.4, "cpc": -2.0, "cpm": 0.6 } } ``` --- # Scraping ## Social Scraping Scrape public profile data and recent media from Instagram or TikTok accounts. Tasks are async -- create a task, then poll for results. `POST /api/v1/scrape/social` ### Request Body | Parameter | Type | Required | Description | | ----------- | --------------------------- | -------- | ----------------------------------- | | handle | string | Yes | Public username to scrape | | platform | `"instagram"` \| `"tiktok"` | Yes | Target platform | | callbackUrl | string | No | Webhook URL to notify on completion | ### Example **Create scrape task** ```bash curl -X POST https://api.wondercat.ai/api/v1/scrape/social \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"handle": "natgeo", "platform": "instagram"}' ``` **Response (201)** ```json { "scrapeTaskId": "st_...", "handle": "natgeo", "platform": "instagram", "status": "pending" } ``` `GET /api/v1/scrape/social/{taskId}` ### Response (completed) Once complete, the task includes the profile and an array of recent media with engagement metrics. ```json { "scrapeTaskId": "st_...", "status": "completed", "profile": { "username": "natgeo", "name": "National Geographic", "bio": "Experience the world through the eyes of National Geographic photographers.", "followerCount": 284000000, "profilePicUrl": "https://..." }, "media": [ { "originalUrl": "https://...", "postUrl": "https://instagram.com/p/...", "caption": "...", "likesCount": 120000, "commentsCount": 3400, "postTimestamp": "2026-03-20T12:00:00Z" } ] } ``` --- ## Meta Ads Library Search the Meta Ad Library for competitor creatives. `POST /api/v1/scrape/meta-ads` ### Request Body | Parameter | Type | Required | Description | | ------------------ | --------------------------------------------------------- | -------- | ---------------------------------------------------- | | query | string | Yes | Search query (brand name, keyword, etc.) | | countryCode | string | Yes | ISO country code (e.g. US, GB, FR) | | searchType | `"page"` \| `"keyword"` | No | Search mode. Defaults to "keyword". | | activeStatus | `"all"` \| `"active"` \| `"inactive"` | No | Filter by active/inactive ads. Defaults to "active". | | sortBy | `"impressions_desc"` \| `"most_recent"` | No | Sort order for returned ads. | | period | `"last24h"` \| `"last7d"` \| `"last14d"` \| `"last30d"` | No | Time window for the search results. | | contentLanguages | string[] | No | Optional language filters. | | publisherPlatforms | string[] | No | Optional publisher platform filters. | | mediaType | `"all"` \| `"image"` \| `"video"` \| `"meme"` \| `"none"` | No | Filter by media type. | | maxResults | number | No | Max ads to return | `GET /api/v1/scrape/meta-ads/{taskId}` ### Response (completed) ```json { "scrapeTaskId": "st_...", "status": "completed", "ads": [ { "title": "Spring Sale", "bodyText": "50% off all items...", "imageUrl": "https://...", "videoUrl": null, "isActive": true, "startDate": "2026-03-01", "endDate": null } ] } ``` --- ## Download a Reel or TikTok `POST /api/v1/scrape/reel` Download a single Instagram Reel or TikTok video by URL. ### Request Body | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------------------- | | url | string | Yes | URL of the Instagram Reel or TikTok | ### Example ```bash curl -X POST https://api.wondercat.ai/api/v1/scrape/reel \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.instagram.com/reel/ABC123"}' ``` ### Response Returns a scrape task ID and platform. Poll `GET /scrape/social/{scrapeTaskId}` until status is `completed`. The downloaded video appears in the media array of the scrape result. Instagram downloads start in `"pending"` status; TikTok downloads start in `"processing"` (sent directly to the video worker). ```json { "scrapeTaskId": "st_...", "platform": "instagram", "status": "pending" } ``` --- # List Styles `GET /api/v1/styles` ## Behavior Returns all styles accessible to the authenticated account. Brand styles (account-specific) are listed first, followed by up to 50 platform-curated featured styles. ## Response Fields | Parameter | Type | Required | Description | | ------------- | ------------------------- | -------- | ------------------------------------------------------------------------------------ | | styleId | string | Yes | UUID of the style | | name | string | Yes | Display name | | description | string \| null | No | Short description of the style (nullable) | | coverImageUrl | string \| null | No | Preview image URL (nullable) | | type | `"brand"` \| `"featured"` | Yes | Whether this is an account-specific brand style or a platform-curated featured style | ## Response ```json { "styles": [ { "styleId": "a1b2c3d4-...", "name": "Brand Cinematic", "description": "Warm cinematic tones with film grain", "coverImageUrl": "https://storage.example.com/styles/brand-cinematic.webp", "type": "brand" }, { "styleId": "e5f6a7b8-...", "name": "Neon Pop", "description": null, "coverImageUrl": null, "type": "featured" } ] } ``` --- # Capabilities `GET /api/v1/capabilities` ## Overview This endpoint is the live source of truth for generation models and publish destinations available on the public API. Static documentation may lag behind, so consult this endpoint when choosing model keys, attachment requirements, publish operations, and params schemas. Editing operation discovery now lives in the CLI because current edits render locally. Use: ```bash wonda operations list wonda operations info ``` The API response still includes an `editing` object for compatibility, but the `video`, `image`, and `audio` arrays are empty. ## Response structure The response is organized into three top-level sections: `generation`, `editing`, and `publish`. ### generation Grouped by media type. Each entry describes a model with its accepted attachments and params JSON Schema. ```json { "generation": { "image": [ { "key": "nano-banana-2", "label": "Nano Banana 2", "description": "General-purpose image generation", "attachments": ["reference_images"], "params": { "type": "object", "properties": {} } } ], "video": [], "text": [] } } ``` ### editing Current editing operations run in the CLI, not through the public API. ```json { "editing": { "video": [], "image": [], "audio": [] } } ``` ### publish Available publish destinations, the media they accept, and params JSON Schema. ```json { "publish": [ { "operation": "instagramPublish", "label": "Instagram Publish", "description": "Publish an image or video directly to Instagram.", "accepts": { "media": { "kinds": ["image", "video"], "min": 1, "max": 1 }, "text": { "optional": true } }, "params": { "type": "object", "properties": {} } } ] } ``` --- # Pricing `GET /api/v1/pricing` ## Overview Returns the current pricing schedule with two sections: `models` and `editor`. All monetary values include microdollars, USD, and credit equivalents. Current `wonda edit` operations render locally in the CLI and do not create server render credit holds. The `editor` field remains in the response for compatibility with older clients and legacy render accounting. ### models Per-model pricing. Each model has a `pricing` array with one entry per variant, such as resolution or quality tier. **Unit values:** `per_second`, `per_image`, `per_character`, `per_render`, `per_generation`, `per_million_input_tokens`, `variable` ```json { "models": [ { "key": "nano-banana-2", "label": "Nano Banana 2", "description": "General-purpose image generation.", "stepType": "image", "pricing": [ { "unit": "per_image", "variant": "1K", "unitPriceMicrodollars": 4000, "unitPriceUsd": "$0.004000", "credits": 4 } ] } ] } ``` ### editor Legacy editor pricing entry retained in the API response. ```json { "editor": { "type": "editor_render", "label": "Video/Image/Audio Editor Render", "pricing": { "unit": "per_render", "unitPriceMicrodollars": 50000, "unitPriceUsd": "$0.050000", "credits": 50 } } } ``` --- # Cost Estimation `POST /api/v1/pricing/estimate` ## Request Body | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------------------------------------- | | model | string | Yes | Model key to estimate cost for | | prompt | string | No | Text prompt (used for token-based cost estimation) | | params | object | No | Model-specific parameters that affect pricing (e.g., resolution, duration) | ## Response ```json { "model": "nano-banana-2", "estimatedCostMicrodollars": 1234, "estimatedCostUsd": "$0.001234", "estimatedCredits": 1234 } ``` ## Example **cURL** ```bash curl https://api.wondercat.ai/api/v1/pricing/estimate \ -H "Authorization: Bearer sk_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{ "model": "nano-banana-2", "params": { "resolution": "2K", "aspectRatio": "16:9" } }' ``` --- # Error Codes ## Error response shape All error responses follow a consistent JSON structure with a human-readable message and a machine-readable code. ```json { "error": { "message": "Human-readable error message", "code": "machine_readable_code" } } ``` ## HTTP status codes | HTTP status | Code | Meaning | | ----------- | ----------------------- | ------------------------------------------------------------------------------------------------ | | 400 | `validation_error` | Invalid request body or parameters. | | 400 | `bad_request` | Request was syntactically valid but cannot be processed. | | 400 | `unknown_project` | `X-Wonda-Project` names a project that does not exist in the active personal or org scope. | | 401 | `unauthorized` | Missing or invalid API key. | | 402 | `payment_required` | Payment required. | | 402 | `insufficient_credits` | Not enough credits to complete the request. | | 403 | `forbidden` | The account cannot access the resource or action. | | 403 | `feature_disabled` | A feature flag or account gate is disabled for this account. | | 404 | `not_found` | Resource not found or API not enabled for the account. | | 409 | `conflict` | Request conflicts with the current state of the resource. | | 409 | `project_exists` | A project with that normalized name already exists in the active scope. | | 410 | `gone` | Endpoint or operation has been retired. The response message may include a migration hint. | | 429 | `rate_limit_exceeded` | Too many requests. | | 429 | `throttled` | A safety or action gate blocked the request and may include retry metadata such as `deferUntil`. | | 500 | `internal_server_error` | Internal server error. | The `code` field is always present in error responses. A 400 from request validation uses `validation_error`; other generic 400 errors use `bad_request`. A 402 uses `insufficient_credits` when the error message mentions insufficient credits, and `payment_required` otherwise. ## Example ```json { "error": { "message": "'model' is required", "code": "validation_error" } } ``` --- # Job Polling ## How it works 1. **Submit** a request, such as `POST /image/generate` or `POST /publish/instagram`. 2. **Poll** `GET /jobs/inference/{inferenceJobId}` or `GET /jobs/publish/{outputJobId}` until the job reaches a terminal status. 3. **Read** output media or publish details from the completed job response. `GET /api/v1/jobs/inference/{inferenceJobId}` `GET /api/v1/jobs/publish/{outputJobId}` Local CLI editing does not create a public API job. The `wonda edit` commands render locally and return a `mediaId` or downloaded file path when the render completes. ## Status values | Status | Terminal | Description | | ------------- | -------- | ----------------------------------- | | `idle` | No | Job is queued and waiting to start. | | `locked` | No | Job has been claimed by a worker. | | `in_progress` | No | Job is actively running. | | `queued` | No | Publish job is waiting in queue. | | `succeeded` | Yes | Job completed successfully. | | `failed` | Yes | Job encountered an error. | | `canceled` | Yes | Inference job was canceled. | ## Polling strategy Start polling at 1-second intervals. For long-running jobs such as video generation, increase the interval up to 5 seconds to reduce unnecessary requests. ## Chaining pattern A typical workflow chains async API work with local CLI editing: ``` upload -> generate -> poll -> take output mediaId -> edit locally with wonda CLI -> publish -> poll ``` ## Full flow example ```javascript const BASE = "https://api.wondercat.ai/api/v1"; const HEADERS = { Authorization: "Bearer sk_your_api_key_here", "Content-Type": "application/json", }; async function pollJob(type, id) { let delay = 1000; while (true) { const job = await fetch(`${BASE}/jobs/${type}/${id}`, { headers: { Authorization: HEADERS.Authorization }, }).then((response) => response.json()); if (job.status === "succeeded") return job; if (job.status === "failed") { throw new Error(job.errorMessage ?? "Job failed"); } if (job.status === "canceled") { throw new Error("Job was canceled"); } await new Promise((resolve) => setTimeout(resolve, delay)); delay = Math.min(delay * 1.5, 5000); } } const generation = await fetch(`${BASE}/image/generate`, { method: "POST", headers: HEADERS, body: JSON.stringify({ model: "nano-banana-2", prompt: "A sunset over the ocean", params: { resolution: "2K" }, }), }).then((response) => response.json()); const generationResult = await pollJob("inference", generation.inferenceJobId); const generated = generationResult.outputs.find( (output) => output.media, )?.media; if (!generated?.mediaId) { throw new Error("No generated media found"); } const publish = await fetch(`${BASE}/publish/instagram`, { method: "POST", headers: HEADERS, body: JSON.stringify({ mediaId: generated.mediaId, instagramAccountId: "550e8400-e29b-41d4-a716-446655440000", caption: "Made with Wonda", product: "IMAGE", }), }).then((response) => response.json()); const publishResult = await pollJob("publish", publish.outputJobId); (() => {})("Publish status:", publishResult.status); ``` --- # Rate Limits ## Request-rate limiting The public API applies per-IP request-rate limits enforced at the edge via Google Cloud Armor. The current buckets are: | Bucket | Limit | Applies to | | ---------------- | ---------------- | ---------------------- | | General | 300 requests/min | Most API requests | | Upload | 300 requests/min | `POST /media/upload` | | Account creation | 15 requests/hour | `POST /auth/temporary` | When the rate limit is exceeded, the API returns a 429 Too Many Requests response. ## Concurrent task admission Some heavy routes do not use request-per-minute limiting. Instead, they are admitted through a separate per-account concurrency and queue policy. This controls direct generation, analysis, and other queued worker workloads. | Plan | Concurrent limit | Queue limit | | -------- | ---------------- | ----------- | | Free | 2 | 25 | | Basic | 5 | 150 | | Wonda | 25 | 100 | | Pro | 15 | 500 | | Absolute | 50 | 2000 | When the queue limit is exceeded, the API rejects the request with a concurrency error that includes the active and queued counts. When the queue still has space, the request may be accepted and started later rather than running immediately. ## What this means in practice - Simple request bursts are governed by the request-rate buckets. - Long-running generation and analysis workloads are governed by the concurrency policy instead. - Local CLI editing runs on your machine, so it is not admitted through the server render queue. - The two systems are separate. A route can be exempt from request-rate limiting and still be subject to concurrency limits.