Studio
The agent slideshow creation path: start a blank deck, then search Pinterest, read a TikTok/Instagram post to remix, browse the library images, and place every background and text box yourself, with the dashboard updating live. The agent is the creative director. (The one-shot server-composed create is a dashboard-only convenience, not exposed here.)
POST /api/v2/pinterest/search
READMCP twin: search_pinterest
Search Pinterest directly (up to 8 queries, default 12 images each) and get raw image urls back. Search for SCENES, not topics. LOOK at the images yourself before picking - that is the whole point of this tool - then set_slide_background with the winner AND the query that found it, so the editor reopens on that search and shuffle can re-run it. 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.
{
"type": "object",
"properties": {
"queries": {
"minItems": 1,
"maxItems": 8,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"description": "Up to 8 Pinterest search queries. Search for SCENES, not topics: candid photos of moments, never concept words that fetch infographics."
},
"perQuery": {
"description": "Images to return per query (default 12).",
"type": "integer",
"minimum": 1,
"maximum": 20
},
"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"
}
},
"required": [
"queries"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"results": [
{
"query": "candid morning skincare routine bathroom mirror",
"imageUrls": [
"https://i.pinimg.com/originals/a1/b2/c3/a1b2c3skincare1.jpg",
"https://i.pinimg.com/originals/d4/e5/f6/d4e5f6skincare2.jpg"
]
}
],
"nextSteps": "LOOK at the candidates before choosing (download and view them); then set_slide_background with the winning url. Images under 720px on the short side will be rejected."
}
}POST /api/v2/social/read
READMCP twin: read_social_post
Read a TikTok Photo Mode or Instagram carousel post so YOU can remix it: returns the caption, slide count, and each slide image url. This is the SOURCE, not a deck. There is no server-side remix; LOOK at the images, then create_blank_slideshow and hand-build - write your own copy in the same structure and cast fresh images. Costs no credits (the deck costs 1 credit on your first build edit).
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.
{
"type": "object",
"properties": {
"url": {
"type": "string",
"minLength": 1,
"maxLength": 2000,
"description": "A TikTok Photo Mode or Instagram post (/p/) URL to read for remixing."
},
"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"
}
},
"required": [
"url"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"platform": "tiktok",
"sourceUrl": "https://www.tiktok.com/@creator/photo/7301234567890123456",
"caption": "the 5 habits that fixed my skin ✨ #skincare #glowup",
"slideCount": 5,
"slides": [
{
"position": 0,
"imageUrl": "https://cdn.ghostfeed.ai/scraped/64f1/s0.jpg"
},
{
"position": 1,
"imageUrl": "https://cdn.ghostfeed.ai/scraped/64f1/s1.jpg"
}
],
"nextSteps": "This is the SOURCE to remix, not your deck. LOOK at the slide images and read the caption to grasp the structure and voice, then create_blank_slideshow with this slideCount and hand-build: write your OWN copy in the same structure (never copy verbatim) and cast FRESH images. The deck costs 1 credit on your first build edit."
}
}GET /api/v2/collections/:collectionId/images
READMCP twin: list_collection_images
List a collection's actual images (urls + AI captions of what each shows), 30 per page. Use to hand-pick a specific image for set_slide_background.
Path parameters: collectionId (from the URL).
{
"type": "object",
"properties": {
"collectionId": {
"type": "string",
"maxLength": 100,
"description": "A collection id from list_collections."
},
"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
},
"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"
}
},
"required": [
"collectionId"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"collection": {
"id": "64f1c2a1b2c3d4e5f6073000",
"name": "Skincare UGC"
},
"images": [
{
"url": "https://cdn.ghostfeed.ai/collections/64f1/img1.jpg",
"caption": "Woman applying serum in a sunlit bathroom"
},
{
"url": "https://cdn.ghostfeed.ai/collections/64f1/img2.jpg"
}
],
"total": 24,
"page": 1,
"pages": 1
}
}GET /api/v2/avatars/:avatarId/images
READMCP twin: list_avatar_images
List a saved avatar's generated photos (urls + captions), newest first, 30 per page. A worked avatar can hold hundreds, so this is paged: read total/pages and ask for the next page rather than expecting everything at once. The captions describe each shot, so use them to pick a specific pose or framing — that is how you cast a first frame for generate_reaction_frames via referenceImageUrl. Reusing them is free; use set_slide_background type 'avatar' to cast one.
Path parameters: avatarId (from the URL).
{
"type": "object",
"properties": {
"avatarId": {
"type": "string",
"maxLength": 100,
"description": "An avatarId from list_avatars."
},
"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
},
"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"
}
},
"required": [
"avatarId"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"avatar": {
"avatarId": "avt_1a2b3c4d5e6f7081",
"name": "Priya"
},
"images": [
{
"url": "https://cdn.ghostfeed.ai/avatars/saved/avt_1a2b3c4d5e6f7081/v1.png",
"caption": "Close-up smiling portrait, natural light"
},
{
"url": "https://cdn.ghostfeed.ai/avatars/saved/avt_1a2b3c4d5e6f7081/v2.png"
}
],
"total": 2,
"page": 1,
"pages": 1
}
}POST /api/v2/slideshows/blank
WRITEMCP twin: create_blank_slideshow
Create an empty slideshow with N blank slides, then build it slide by slide with set_slide_background and set_slide_texts. This is how an agent creates a deck: YOU do the creative work. The empty deck is free; the deck costs 1 credit, charged on your first set_slide_background or set_slide_texts (same price as a dashboard-made deck). Later edits are free.
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.
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 120,
"description": "The slideshow name shown in the dashboard."
},
"slideCount": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"description": "How many blank slides to start with (add/remove later as needed)."
},
"workspace": {
"type": "string",
"minLength": 1,
"description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
}
},
"required": [
"name",
"slideCount",
"workspace"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"slideshow": {
"id": "64f1c2a1b2c3d4e5f6072500",
"name": "Untitled Skincare Deck",
"dashboardUrl": "https://ghostfeed.ai/w/client-a/slideshows/64f1c2a1b2c3d4e5f6072500?ref=agent"
},
"total": 1,
"page": 1,
"pages": 1,
"nextSteps": "Build it slide by slide: search_pinterest (or list_collection_images / list_avatar_images) for backgrounds, set_slide_background + set_slide_texts per slide, set_caption at the end. The user sees every change live in the dashboard."
}
}POST /api/v2/slideshows/image-upload-url
WRITEIDEMPOTENTMCP twin: request_slide_image_upload
Get a short-lived presigned R2 upload URL for a LOCAL image file (too large to pass as a tool argument). PUT the raw bytes to uploadUrl with the given Content-Type, then call set_slide_background with background.type "url" and background.url = the returned fileUrl. 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.
{
"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#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"upload": {
"method": "PUT",
"uploadUrl": "https://abc123.r2.cloudflarestorage.com/shortsbro/users/1a2b3c4d/editor/images/2026/07/20/uuid.jpg?X-Amz-Signature=...",
"fileUrl": "https://cdn.ghostfeed.ai/users/1a2b3c4d/editor/images/2026/07/20/uuid.jpg",
"contentType": "image/jpeg",
"expiresInSeconds": 900
},
"nextSteps": "PUT the bytes to uploadUrl, then set_slide_background with type 'url' and url=fileUrl."
}
}PUT /api/v2/slideshows/:slideshowId/slides/:slideIndex/background
WRITEMCP twin: set_slide_background
Set one slide's background: a Pinterest url, a collection image, an avatar photo, any image url, or a solid color. On a Pinterest pick, pass background.query (the search_pinterest query that found it) so the editor's Pinterest tab reopens on that search. For a LOCAL image file, call request_slide_image_upload first, PUT the bytes, then pass the returned fileUrl here as type 'url'. External urls are re-hosted to durable storage automatically. Image picks are gated server-side (browser-displayable, 720px+ short side). The dashboard updates live. If this is the deck's first build edit it charges 1 credit and returns creditsSpent/creditsRemaining.
Path parameters: slideshowId, slideIndex (from the URL).
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.
{
"type": "object",
"properties": {
"slideshowId": {
"type": "string",
"maxLength": 100,
"description": "The slideshow (project) id."
},
"variantIndex": {
"description": "Which variant to edit (1 = the base deck; higher indexes come from create_variant). Omit for the base.",
"type": "integer",
"minimum": 1,
"maximum": 10
},
"slideIndex": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The slide position, 0-based."
},
"background": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"pinterest",
"collection",
"avatar",
"url",
"color"
],
"description": "Where this background comes from. 'pinterest' = a url from search_pinterest; 'collection'/'avatar' = a url from the matching list tool (id required); 'url' = any other image url, INCLUDING a fileUrl from request_slide_image_upload (an external url is re-hosted to durable storage automatically); 'color' = solid background. To use a LOCAL image file, call request_slide_image_upload first, PUT the bytes, then set type 'url' with the returned fileUrl."
},
"url": {
"description": "The image url (types 'pinterest'/'collection'/'avatar'/'url').",
"type": "string",
"maxLength": 2000
},
"color": {
"description": "Hex color like #101010 (type 'color' only).",
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$"
},
"collectionId": {
"description": "Required when type is 'collection'.",
"type": "string",
"maxLength": 100
},
"avatarId": {
"description": "Required when type is 'avatar'.",
"type": "string",
"maxLength": 100
},
"query": {
"description": "The search_pinterest query that found this image (type 'pinterest'). Pass it: it is what the editor's Pinterest tab reopens with and what shuffle re-runs. Omit it and the user gets an empty search box on that slide.",
"type": "string",
"maxLength": 200
},
"candidateUrls": {
"description": "The other image urls that same search returned (type 'pinterest'), so the editor reopens with the full candidate set instead of re-searching.",
"maxItems": 50,
"type": "array",
"items": {
"type": "string",
"maxLength": 2000
}
}
},
"required": [
"type"
],
"description": "The background to set."
},
"transform": {
"description": "Optional reposition/zoom of the background within the canvas.",
"type": "object",
"properties": {
"x": {
"type": "number"
},
"y": {
"type": "number"
},
"scale": {
"type": "number",
"minimum": 0.1,
"maximum": 10
}
},
"required": [
"x",
"y",
"scale"
]
},
"workspace": {
"type": "string",
"minLength": 1,
"description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
}
},
"required": [
"slideshowId",
"slideIndex",
"background",
"workspace"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"slide": {
"position": 0,
"backgroundSource": "pinterest"
},
"creditsSpent": 1,
"creditsRemaining": 46
}
}PUT /api/v2/slideshows/:slideshowId/slides/:slideIndex/texts
WRITEMCP twin: set_slide_texts
Replace one slide's text boxes (1-5 texts with placement top/center/bottom and role heading/subheading/body/cta - role sets the font size, positions are computed server-side exactly like the dashboard). Optionally mark isProductSlide. If this is the deck's first build edit it charges 1 credit and returns creditsSpent/creditsRemaining.
Path parameters: slideshowId, slideIndex (from the URL).
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.
{
"type": "object",
"properties": {
"slideshowId": {
"type": "string",
"maxLength": 100,
"description": "The slideshow (project) id."
},
"variantIndex": {
"description": "Which variant to edit (1 = the base deck; higher indexes come from create_variant). Omit for the base.",
"type": "integer",
"minimum": 1,
"maximum": 10
},
"slideIndex": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The slide position, 0-based."
},
"texts": {
"minItems": 1,
"maxItems": 5,
"type": "array",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"minLength": 1,
"maxLength": 500
},
"placement": {
"type": "string",
"enum": [
"top",
"center",
"bottom"
],
"description": "Vertical zone on the slide."
},
"role": {
"type": "string",
"enum": [
"heading",
"subheading",
"body",
"cta"
],
"description": "Typographic job; sets the font size (heading largest)."
}
},
"required": [
"text",
"placement",
"role"
]
},
"description": "Replaces ALL text boxes on the slide; positions are computed server-side."
},
"isProductSlide": {
"description": "Mark/unmark this slide as the product slide (UI badge).",
"type": "boolean"
},
"workspace": {
"type": "string",
"minLength": 1,
"description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
}
},
"required": [
"slideshowId",
"slideIndex",
"texts",
"workspace"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"slide": {
"position": 0,
"texts": [
"5 morning habits",
"that changed my skin"
]
}
}
}POST /api/v2/slideshows/:slideshowId/slides
WRITEMCP twin: add_slide
Insert a blank slide (append by default). Decks cap at 12 slides.
Path parameters: slideshowId (from the URL).
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.
{
"type": "object",
"properties": {
"slideshowId": {
"type": "string",
"maxLength": 100,
"description": "The slideshow (project) id."
},
"variantIndex": {
"description": "Which variant to edit (1 = the base deck; higher indexes come from create_variant). Omit for the base.",
"type": "integer",
"minimum": 1,
"maximum": 10
},
"position": {
"description": "Insert position (0-based); omit to append at the end.",
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"workspace": {
"type": "string",
"minLength": 1,
"description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
}
},
"required": [
"slideshowId",
"workspace"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"slideCount": 7,
"position": 3
}
}DELETE /api/v2/slideshows/:slideshowId/slides/:slideIndex
WRITEMCP twin: remove_slide
Remove a slide by position. A deck keeps at least one slide.
Path parameters: slideshowId, slideIndex (from the URL).
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.
{
"type": "object",
"properties": {
"slideshowId": {
"type": "string",
"maxLength": 100,
"description": "The slideshow (project) id."
},
"variantIndex": {
"description": "Which variant to edit (1 = the base deck; higher indexes come from create_variant). Omit for the base.",
"type": "integer",
"minimum": 1,
"maximum": 10
},
"slideIndex": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The slide position to remove, 0-based."
},
"workspace": {
"type": "string",
"minLength": 1,
"description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
}
},
"required": [
"slideshowId",
"slideIndex",
"workspace"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"slideCount": 6
}
}PUT /api/v2/slideshows/:slideshowId/slides/order
WRITEMCP twin: reorder_slides
Reorder slides with a permutation of current positions, e.g. [2,0,1].
Path parameters: slideshowId (from the URL).
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.
{
"type": "object",
"properties": {
"slideshowId": {
"type": "string",
"maxLength": 100,
"description": "The slideshow (project) id."
},
"variantIndex": {
"description": "Which variant to edit (1 = the base deck; higher indexes come from create_variant). Omit for the base.",
"type": "integer",
"minimum": 1,
"maximum": 10
},
"order": {
"minItems": 1,
"maxItems": 12,
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"description": "The new order as a permutation of current positions, e.g. [2,0,1] makes the current slide 2 first."
},
"workspace": {
"type": "string",
"minLength": 1,
"description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
}
},
"required": [
"slideshowId",
"order",
"workspace"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"slideCount": 6
}
}PUT /api/v2/slideshows/:slideshowId/caption
WRITEMCP twin: set_caption
Set the slideshow's post caption.
Path parameters: slideshowId (from the URL).
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.
{
"type": "object",
"properties": {
"slideshowId": {
"type": "string",
"maxLength": 100,
"description": "The slideshow (project) id."
},
"variantIndex": {
"description": "Which variant to edit (1 = the base deck; higher indexes come from create_variant). Omit for the base.",
"type": "integer",
"minimum": 1,
"maximum": 10
},
"caption": {
"type": "string",
"maxLength": 2200,
"description": "The post caption (empty string clears it)."
},
"workspace": {
"type": "string",
"minLength": 1,
"description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
}
},
"required": [
"slideshowId",
"caption",
"workspace"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"caption": "the 5 habits that finally fixed my skin ✨ #skincare"
}
}POST /api/v2/slideshows/:slideshowId/variants
WRITEMCP twin: create_variant
Add a variant to a slideshow: a COPY of the base deck, returned with a new variantIndex. Differentiate it the same way the dashboard shuffles variants, by passing that variantIndex to set_slide_background / set_slide_texts per slide: re-cast a FRESH image on each slide (search_pinterest + a new pick for Pinterest slides; a different image from the same collection/avatar for those) and REWRITE the copy. Keep uploaded, plain-url, and AI-generated images as-is. Costs 1 credit (same as a dashboard-generated variant); returns creditsSpent/creditsRemaining. Editing the variant afterwards is free.
Path parameters: slideshowId (from the URL).
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.
{
"type": "object",
"properties": {
"slideshowId": {
"type": "string",
"maxLength": 100,
"description": "The slideshow (project) id to add a variant to."
},
"workspace": {
"type": "string",
"minLength": 1,
"description": "Required write target: a workspace slug or id from list_workspaces. Writes never inherit the connection default."
}
},
"required": [
"slideshowId",
"workspace"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}{
"success": true,
"data": {
"workspace": {
"id": "6a18c36ee586c7e44e4ece67",
"name": "Client A",
"slug": "client-a"
},
"slideshowId": "64f1c2a1b2c3d4e5f6072000",
"variantIndex": 2,
"creditsSpent": 1,
"creditsRemaining": 45
}
}