GhostfeedGhostfeed MCPv5.3.0
REST API

UGC Reactions

Create UGC reaction videos over the two-phase flow: import a template (link, local upload, or an inspiration clip), render an avatar into the source pose (the first frame), then, after the user approves the frame, animate it — cloning the reference motion or from a text prompt. Bulk creation only; cropping, editing, and deletion stay in the dashboard.

GET /api/v2/reaction-templates

READ

MCP twin: list_reaction_templates

FIRST source search for every new reaction request unless the user explicitly supplied an exact source. List reaction TEMPLATES usable in this workspace: platform stock plus your own imported/uploaded motion clips. Each has an id, name, category, duration, thumbnail, and status. Captioned clips also carry opensOn (the opening still your avatar gets rendered into) and motion (what happens in the clip); older clips may not have them yet. Choose primarily on opensOn. A prompt-mode video may reuse the opening pose without copying the source motion. Use motion only to judge a requested clone, and never paste it into generate_reaction_video as the prompt. If no template has a reasonable opening-pose match, search list_inspiration_reactions next.

Query parameters
{
  "type": "object",
  "properties": {
    "workspace": {
      "description": "Target workspace: a slug, id, or exact name from list_workspaces. Omit to use this connection's default workspace. Must be inside the credential's grant and your current access; overrides the connection-level X-Workspace-Id header.",
      "type": "string"
    },
    "mine": {
      "type": "boolean"
    },
    "includeUnusable": {
      "description": "Include templates that cannot be generated from yet: ones still importing, ones that failed, and ones over 30s awaiting a crop. Off by default, because generating from them is refused anyway.",
      "type": "boolean"
    },
    "page": {
      "description": "Page number (default 1).",
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "limit": {
      "description": "Per page (max 25; lower than other tools because these rows carry long descriptions). Each tool documents its own default.",
      "type": "integer",
      "minimum": 1,
      "maximum": 25
    }
  },
  "$schema": "http://json-schema.org/draft-07/schema#"
}
Example response
{
  "success": true,
  "data": {
    "workspace": {
      "id": "6a18c36ee586c7e44e4ece67",
      "name": "Client A",
      "slug": "client-a"
    },
    "templates": [
      {
        "id": "user-1a2b3c4d-1737100000000",
        "name": "Excited point",
        "category": "Reactions",
        "durationSeconds": 4.2,
        "thumbnailUrl": "https://cdn.ghostfeed.ai/reactions/user-1a2b3c4d/thumbnail.jpg",
        "kind": "owned",
        "status": "completed",
        "mention": "@template:Excited point (user-1a2b3c4d-1737100000000)",
        "opensOn": "Medium shot of a woman at a desk, looking just off camera, hands raised mid-gesture.",
        "motion": "She points at the camera, leans in, and breaks into a grin."
      }
    ],
    "total": 1,
    "page": 1,
    "pages": 1,
    "nextSteps": "Pick a template, then generate_reaction_frames with its id and one or more avatars."
  }
}

GET /api/v2/reaction-frames

READ

MCP twin: list_reaction_frames

List the reaction FRAMES on the workspace board, newest first, including ones a person made in the dashboard rather than through an agent. Each has a frameId, image, the avatar it used, its source template, the videoStage of the shell it lives in, and isAnimated (false means nothing has been animated from it yet, so it is free to use). Use this to act on existing work: "animate the frame I made this morning" starts here, then pass the frameId to generate_reaction_video.

Query parameters
{
  "type": "object",
  "properties": {
    "workspace": {
      "description": "Target workspace: a slug, id, or exact name from list_workspaces. Omit to use this connection's default workspace. Must be inside the credential's grant and your current access; overrides the connection-level X-Workspace-Id header.",
      "type": "string"
    },
    "avatar": {
      "type": "string",
      "minLength": 1
    },
    "templateId": {
      "type": "string",
      "minLength": 1
    },
    "page": {
      "description": "Page number (default 1).",
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "limit": {
      "description": "Per page (max 100). Each tool documents its own default.",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    }
  },
  "$schema": "http://json-schema.org/draft-07/schema#"
}
Example response
{
  "success": true,
  "data": {
    "workspace": {
      "id": "6a18c36ee586c7e44e4ece67",
      "name": "Client A",
      "slug": "client-a"
    },
    "frames": [
      {
        "frameId": "4b706a83-c035-427d-9f68-1091101c6dec",
        "imageUrl": "https://cdn.ghostfeed.ai/frames/frame-1737100000000-1.png",
        "avatarId": "d7d06ea3-a574-4fe0-9a39-d189a0b68698",
        "avatarName": "Ashley Gram",
        "templateId": "user-1a2b3c4d-1737100000000",
        "videoId": "1cc27551-5ad9-42bd-9a9c-899f83dcfec6",
        "videoStage": "selecting-frame",
        "isAnimated": false,
        "createdAt": "2026-07-20T02:22:50.374Z",
        "mention": "@frame:Ashley Gram (4b706a83-c035-427d-9f68-1091101c6dec)",
        "dashboardUrl": "https://app.ghostfeed.ai/w/client-a/ugc-reactions?frame=4b706a83-c035-427d-9f68-1091101c6dec"
      }
    ],
    "total": 1,
    "page": 1,
    "pages": 1,
    "nextSteps": "Pass a frameId to generate_reaction_video to animate it."
  }
}

GET /api/v2/reaction-videos

READ

MCP twin: list_reaction_videos

List the reaction VIDEOS in this workspace, newest first, agent-made or not. Each has a videoId, name, stage, the avatar, and a playable videoUrl once stage is "complete". Use it to report what is finished or still rendering. Managing and deleting videos stays in the dashboard.

Query parameters
{
  "type": "object",
  "properties": {
    "workspace": {
      "description": "Target workspace: a slug, id, or exact name from list_workspaces. Omit to use this connection's default workspace. Must be inside the credential's grant and your current access; overrides the connection-level X-Workspace-Id header.",
      "type": "string"
    },
    "avatar": {
      "type": "string",
      "minLength": 1
    },
    "stage": {
      "type": "string",
      "minLength": 1
    },
    "page": {
      "description": "Page number (default 1).",
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "limit": {
      "description": "Per page (max 100). Each tool documents its own default.",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    }
  },
  "$schema": "http://json-schema.org/draft-07/schema#"
}
Example response
{
  "success": true,
  "data": {
    "workspace": {
      "id": "6a18c36ee586c7e44e4ece67",
      "name": "Client A",
      "slug": "client-a"
    },
    "videos": [
      {
        "videoId": "b9b2a07c-2119-455f-aebf-e7d0b2f32182",
        "name": "Excited point - Ashley Gram",
        "stage": "complete",
        "videoUrl": "https://cdn.ghostfeed.ai/generated-videos/b9b2a07c/video.mp4",
        "avatarId": "d7d06ea3-a574-4fe0-9a39-d189a0b68698",
        "avatarName": "Ashley Gram",
        "createdAt": "2026-07-20T02:09:51.871Z",
        "dashboardUrl": "https://app.ghostfeed.ai/w/client-a/ugc-reactions?video=b9b2a07c-2119-455f-aebf-e7d0b2f32182"
      }
    ],
    "total": 1,
    "page": 1,
    "pages": 1,
    "nextSteps": "stage \"complete\" means the clip is finished and videoUrl plays it."
  }
}

GET /api/v2/inspiration-reactions

READ

MCP twin: list_inspiration_reactions

SECOND source search for every new reaction request, after list_reaction_templates has no suitable opening pose. Browse the curated inspiration library of reaction clips (ids, names, categories, thumbnails). Captioned clips also carry opensOn (the opening still your avatar gets rendered into) and motion (what happens in the clip). Choose primarily on opensOn. A prompt-mode video may reuse the opening pose without copying the source motion. Use motion only to judge a requested clone, and never paste it into generate_reaction_video as the prompt. Pass an inspiration id to import_reaction_template or generate_reaction_frames and it is auto-saved to this workspace as a template first. Only fall back to a custom referenceImageUrl after both libraries lack a reasonable match.

Query parameters
{
  "type": "object",
  "properties": {
    "workspace": {
      "description": "Target workspace: a slug, id, or exact name from list_workspaces. Omit to use this connection's default workspace. Must be inside the credential's grant and your current access; overrides the connection-level X-Workspace-Id header.",
      "type": "string"
    },
    "category": {
      "type": "string",
      "maxLength": 80
    },
    "page": {
      "description": "Page number (default 1).",
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "limit": {
      "description": "Per page (max 100). Each tool documents its own default.",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    }
  },
  "$schema": "http://json-schema.org/draft-07/schema#"
}
Example response
{
  "success": true,
  "data": {
    "workspace": {
      "id": "6a18c36ee586c7e44e4ece67",
      "name": "Client A",
      "slug": "client-a"
    },
    "inspiration": [
      {
        "id": "insp_9f8e7d6c",
        "name": "Grandma tastes pickle",
        "category": "Food",
        "durationSeconds": 6.1,
        "thumbnailUrl": "https://cdn.ghostfeed.ai/inspiration/insp_9f8e7d6c/thumb.jpg",
        "mention": "@inspiration:Grandma tastes pickle (insp_9f8e7d6c)"
      }
    ],
    "total": 1,
    "page": 1,
    "pages": 1,
    "nextSteps": "Pass an inspiration id to generate_reaction_frames; it is auto-saved as a template."
  }
}

GET /api/v2/reaction-video-modes

READ

MCP twin: list_reaction_video_modes

List the video modes for the reaction video phase, with per-second credit cost and whether each needs a prompt or supports audio. Two families: CLONE modes reproduce a template motion (default one_to_one_standard, Kling motion-control, no prompt); PROMPT modes animate the frame from text (default seedance_2_0_fast). Use a clone mode only when the user wants to reproduce the reference video motion.

No parameters.

Example response
{
  "success": true,
  "data": {
    "modes": [
      {
        "mode": "one_to_one_standard",
        "label": "1:1 Standard",
        "family": "clone",
        "creditsPerSecond": 1,
        "supportsAudio": false,
        "needsPrompt": false,
        "isDefault": true
      },
      {
        "mode": "seedance_2_0_fast",
        "label": "Seedance 2.0 Fast",
        "family": "prompt",
        "creditsPerSecond": 1.5,
        "supportsAudio": false,
        "needsPrompt": true,
        "durationOptions": [
          4,
          5,
          6,
          7,
          8,
          9,
          10,
          11,
          12,
          13,
          14,
          15
        ],
        "supportsAutoDuration": true,
        "isDefault": true
      }
    ],
    "defaults": {
      "clone": "one_to_one_standard",
      "prompt": "seedance_2_0_fast"
    },
    "nextSteps": "Clone modes reuse a template motion; prompt modes animate from text."
  }
}

POST /api/v2/reactions/upload-url

WRITEIDEMPOTENT

MCP twin: request_reaction_upload

Get a short-lived presigned R2 upload URL for a LOCAL video file (a video is too large for a tool argument). PUT the raw bytes to uploadUrl with the given Content-Type, then pass the returned fileUrl to import_reaction_template as uploadedFileUrl. Costs no credits.

Workspace-scoped writes name workspace in the body (or as a ?workspace= query param on a bodyless DELETE); the X-Workspace-Id header is for reads only.

Request body
{
  "type": "object",
  "properties": {
    "workspace": {
      "type": "string",
      "minLength": 1,
      "description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
    },
    "filename": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "contentType": {
      "type": "string",
      "minLength": 3,
      "maxLength": 100
    }
  },
  "required": [
    "workspace"
  ],
  "$schema": "http://json-schema.org/draft-07/schema#"
}
Example response
{
  "success": true,
  "data": {
    "workspace": {
      "id": "6a18c36ee586c7e44e4ece67",
      "name": "Client A",
      "slug": "client-a"
    },
    "upload": {
      "method": "PUT",
      "uploadUrl": "https://abc123.r2.cloudflarestorage.com/shortsbro/users/1a2b3c4d/reactions-uploads/2026/07/18/uuid.mp4?X-Amz-Signature=...",
      "fileUrl": "https://cdn.ghostfeed.ai/users/1a2b3c4d/reactions-uploads/2026/07/18/uuid.mp4",
      "contentType": "video/mp4",
      "expiresInSeconds": 900
    },
    "nextSteps": "PUT the bytes to uploadUrl, then import_reaction_template with uploadedFileUrl=fileUrl."
  }
}

POST /api/v2/reaction-templates/import

WRITE

MCP twin: import_reaction_template

Import a reaction TEMPLATE from exactly one source: sourceUrl (a TikTok/Instagram link), uploadedFileUrl (from request_reaction_upload, the local-file path), or inspirationId (a curated clip). Async: returns a reaction_import generation to poll with get_generation. succeeded = ready to use; needs_action = the source is 30-120s and must be cropped in the dashboard first (open dashboardUrl); >120s is rejected. Costs no credits. Managing, cropping, and deleting templates all happen in the dashboard.

Workspace-scoped writes name workspace in the body (or as a ?workspace= query param on a bodyless DELETE); the X-Workspace-Id header is for reads only.

Request body
{
  "type": "object",
  "properties": {
    "workspace": {
      "type": "string",
      "minLength": 1,
      "description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "category": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    },
    "sourceUrl": {
      "type": "string",
      "format": "uri"
    },
    "uploadedFileUrl": {
      "type": "string",
      "format": "uri"
    },
    "inspirationId": {
      "type": "string",
      "minLength": 1
    },
    "idempotencyKey": {
      "description": "A stable key to reuse if you retry this exact request; a repeat with the same key is NOT charged again.",
      "type": "string",
      "maxLength": 200
    }
  },
  "required": [
    "workspace",
    "name"
  ],
  "$schema": "http://json-schema.org/draft-07/schema#"
}
Example response
{
  "success": true,
  "data": {
    "workspace": {
      "id": "6a18c36ee586c7e44e4ece67",
      "name": "Client A",
      "slug": "client-a"
    },
    "generation": {
      "id": "gen_5b6c7d8e9f0a",
      "kind": "reaction_import",
      "state": "running",
      "title": "Excited point",
      "input": {
        "templateId": "user-1a2b3c4d-1737100000000",
        "source": "link"
      },
      "outputs": [],
      "dashboardUrl": "https://ghostfeed.ai/w/client-a/ugc-reactions?template=user-1a2b3c4d-1737100000000&ref=agent",
      "createdAt": "2026-07-18T12:00:00.000Z",
      "updatedAt": "2026-07-18T12:00:00.000Z"
    },
    "templateId": "user-1a2b3c4d-1737100000000",
    "mention": "@template:Excited point (user-1a2b3c4d-1737100000000)",
    "nextSteps": "Poll get_generation until terminal. needs_action = crop in the dashboard first."
  }
}

POST /api/v2/reactions/frames

WRITE

MCP twin: generate_reaction_frames

PHASE 1 of a UGC reaction: render one or more avatars into the source pose, ONE frame per avatar. LIBRARY-FIRST RULE: unless the user explicitly supplied an exact source, call list_reaction_templates first, then list_inspiration_reactions if needed, and use a reasonable library opening-pose match. A custom motion request can still use a library first frame with prompt-mode animation. Use referenceImageUrl only after both Ghostfeed libraries lack a suitable opening pose, and tell the user why the custom fallback was necessary. Source is exactly one of templateId, inspirationId (auto-saved to a template first), or referenceImageUrl. Async: returns one reaction_frame generation per avatar to poll; the finished frame is output.url and output.id is the frameId. Charges the image-model cost per avatar as frames render (gemini_flash unless imageModel is set). STOP after this: show the frame(s) to the user and get approval BEFORE generate_reaction_video, because video credits are far higher.

Workspace-scoped writes name workspace in the body (or as a ?workspace= query param on a bodyless DELETE); the X-Workspace-Id header is for reads only.

Request body
{
  "type": "object",
  "properties": {
    "workspace": {
      "type": "string",
      "minLength": 1,
      "description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
    },
    "avatars": {
      "minItems": 1,
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "templateId": {
      "type": "string",
      "minLength": 1
    },
    "inspirationId": {
      "type": "string",
      "minLength": 1
    },
    "referenceImageUrl": {
      "type": "string",
      "format": "uri"
    },
    "imageModel": {
      "type": "string",
      "enum": [
        "gemini_flash_lite",
        "gemini_flash",
        "gemini_pro",
        "gpt_low",
        "gpt_medium",
        "gpt_high"
      ]
    },
    "stylingPrompt": {
      "type": "string",
      "maxLength": 500
    },
    "aspectRatio": {
      "type": "string",
      "enum": [
        "9:16",
        "16:9",
        "1:1"
      ]
    },
    "idempotencyKey": {
      "description": "A stable key to reuse if you retry this exact request; a repeat with the same key is NOT charged again.",
      "type": "string",
      "maxLength": 200
    }
  },
  "required": [
    "workspace",
    "avatars"
  ],
  "$schema": "http://json-schema.org/draft-07/schema#"
}
Example response
{
  "success": true,
  "data": {
    "workspace": {
      "id": "6a18c36ee586c7e44e4ece67",
      "name": "Client A",
      "slug": "client-a"
    },
    "generations": [
      {
        "id": "gen_frame_aa11bb22",
        "kind": "reaction_frame",
        "state": "running",
        "phase": "generating-frames",
        "input": {
          "videoId": "vid_aa11bb22",
          "source": "template",
          "imageModel": "gemini_flash"
        },
        "outputs": [],
        "dashboardUrl": "https://ghostfeed.ai/w/client-a/ugc-reactions?ref=agent",
        "createdAt": "2026-07-18T12:01:00.000Z",
        "updatedAt": "2026-07-18T12:01:00.000Z"
      }
    ],
    "estimatedCreditCost": 1,
    "creditsRemaining": 46,
    "nextSteps": "Poll each generation; the frame image is output.url. Get the user to approve before generate_reaction_video."
  }
}

POST /api/v2/reactions/videos

WRITE

MCP twin: generate_reaction_video

PHASE 2 of a UGC reaction: animate approved frame(s) into video. Only call after the user has reviewed and approved the frames. mode picks the family: a CLONE mode (one_to_one_*) copies the motion straight from the source clip and takes no prompt; a PROMPT mode (default seedance_2_0_fast) animates from text and needs a prompt. In PROMPT mode the source template supplied only the first frame — the movement comes from what the USER asked for, so write prompt from their words. Never paste the template motion text in as the prompt: that field exists to help you CHOOSE a clip, and using it would animate the reference performance instead of the one the user described. Duration is auto unless durationSeconds is set (ignored by clone modes, which follow the source clip). Async: returns one reaction_video generation per frame to poll; the clip is output.url. Charges per second of video and reports creditsSpent/creditsRemaining.

Workspace-scoped writes name workspace in the body (or as a ?workspace= query param on a bodyless DELETE); the X-Workspace-Id header is for reads only.

Request body
{
  "type": "object",
  "properties": {
    "workspace": {
      "type": "string",
      "minLength": 1,
      "description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
    },
    "frameIds": {
      "minItems": 1,
      "maxItems": 8,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "mode": {
      "type": "string",
      "enum": [
        "one_to_one_standard",
        "one_to_one_clone_premium",
        "grok",
        "pixverse",
        "pixverse_1080p",
        "kling_2_5_turbo",
        "seedance_2_0",
        "seedance_2_0_fast",
        "seedance_2_0_mini"
      ]
    },
    "prompt": {
      "type": "string",
      "maxLength": 2000
    },
    "durationSeconds": {
      "type": "number",
      "exclusiveMinimum": 0
    },
    "enableAudio": {
      "type": "boolean"
    },
    "maxCredits": {
      "description": "Refuse to start any clip that would cost more than this many credits. Clone modes take their length from the source clip, so a 30s template is 30 credits at 1 credit/second and 60 at 2. A ceiling of MAX_REACTION_VIDEO_CREDITS credits per clip always applies; this only tightens it.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 9007199254740991
    },
    "idempotencyKey": {
      "description": "A stable key to reuse if you retry this exact request; a repeat with the same key is NOT charged again.",
      "type": "string",
      "maxLength": 200
    }
  },
  "required": [
    "workspace",
    "frameIds"
  ],
  "$schema": "http://json-schema.org/draft-07/schema#"
}
Example response
{
  "success": true,
  "data": {
    "workspace": {
      "id": "6a18c36ee586c7e44e4ece67",
      "name": "Client A",
      "slug": "client-a"
    },
    "generations": [
      {
        "id": "gen_vid_cc33dd44",
        "kind": "reaction_video",
        "state": "running",
        "phase": "generating-video",
        "input": {
          "videoId": "vid_aa11bb22",
          "frameId": "frm_ee55ff66",
          "mode": "seedance_2_0_fast"
        },
        "outputs": [],
        "dashboardUrl": "https://ghostfeed.ai/w/client-a/ugc-reactions?video=vid_aa11bb22&ref=agent",
        "createdAt": "2026-07-18T12:05:00.000Z",
        "updatedAt": "2026-07-18T12:05:00.000Z"
      }
    ],
    "skipped": [],
    "creditsSpent": 8,
    "creditsRemaining": 38,
    "nextSteps": "Poll each generation until its state is succeeded; the clip is output.url. Report spend to the user."
  }
}

POST /api/v2/reactions/frames/regenerate

WRITE

MCP twin: regenerate_reaction_frame

Redo a frame the user was not happy with. Identify it with EITHER frameId (any frame from list_reaction_frames, including ones a person made in the dashboard) OR generationId (a frame this API started); pass exactly one. Re-runs frame generation on the same avatar + source and charges another frame credit. Poll the same generation until it succeeds with a fresh frame, then get approval before generate_reaction_video.

Workspace-scoped writes name workspace in the body (or as a ?workspace= query param on a bodyless DELETE); the X-Workspace-Id header is for reads only.

Request body
{
  "type": "object",
  "properties": {
    "workspace": {
      "type": "string",
      "minLength": 1,
      "description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
    },
    "generationId": {
      "description": "The generationId of a frame you started through the agent API.",
      "type": "string",
      "minLength": 1
    },
    "frameId": {
      "description": "A frameId from list_reaction_frames. Use this for any frame, including ones a person made in the dashboard, which have no generationId. Pass exactly one of the two.",
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "workspace"
  ],
  "$schema": "http://json-schema.org/draft-07/schema#"
}
Example response
{
  "success": true,
  "data": {
    "workspace": {
      "id": "6a18c36ee586c7e44e4ece67",
      "name": "Client A",
      "slug": "client-a"
    },
    "generation": {
      "id": "gen_frame_aa11bb22",
      "kind": "reaction_frame",
      "state": "running",
      "phase": "generating-frames",
      "input": {
        "videoId": "vid_aa11bb22",
        "source": "template",
        "imageModel": "gemini_flash"
      },
      "outputs": [],
      "dashboardUrl": "https://ghostfeed.ai/w/client-a/ugc-reactions?ref=agent",
      "createdAt": "2026-07-18T12:01:00.000Z",
      "updatedAt": "2026-07-18T12:07:00.000Z"
    },
    "nextSteps": "Poll until it succeeds with a fresh frame, then get approval before the video."
  }
}