AI ad generator: make image and video ads for your brand and research the ads competitors run.
Maintain this server? Claim it
Call this server's tools from your browser. Nothing to install.
Requests go through mcptry because browsers can't call most MCP servers directly. Header values are sent to the server for that request only and are never stored or logged.
Tool list from our last check. Connect live to refresh it.
Put a PRESENTER on ANY video, only when the user asks for one: their pick of person (`creator`: a saved or preset creator from list_creators; or `image`: a photo they gave; never invent one), filmed on a green screen saying the words, cut out and laid over the video in a bottom corner (default), a round badge, or split screen, lip-synced, block by block across the whole video (about 10 s and about 25 spoken words a block). The words are `script` (verbatim; refused free if it cannot fit) or, without one, written to fit from what the video says and shows (`brief` steers them). The video's own sound stays under the presenter unless baseAudio 'off'; the video without the presenter is kept. A face the video model refuses STOPS it with nothing charged: offer another creator, another photo, or the video as it is. dryRun:true = the exact price, which is what it charges. For a finished make_explainer film, make_explainer with fromFilm.
the video: an https / upload_file URL, a Library id (L0…), or a local path on the CLI; up to 2 minutes
without a script: what they should talk about
or a photo of the presenter (a real person's face follows the real-face rules)
0.2-0.8 of the frame
a voice name, only when asked (default: their own voice)
price only: nothing made, nothing charged
the exact words to say, used verbatim and split at sentence ends
the presenter: a saved or preset creator, by name or id
subtitles of what they say; only when asked
'br' (default), 'bl', 'tr', 'tl'
the video's own sound: 'under' the presenter (default) or 'off'
This tool isn't marked read-only, so it may change something on the server or in an account you connected.
Pick your client. Values in capitals, like YOUR_TOKEN, are yours to fill in.
Run in a terminal
claude mcp add --transport http hermoso-ai-ad-generator 'https://app.hermoso.ai/mcp?tools=app-create&src=registry'
Put a PRESENTER on ANY video, only when the user asks for one: their pick of person (`creator`: a saved or preset creator from list_creators; or `image`: a photo they gave; never invent one), filmed on a green screen saying the words, cut out and laid over the video in a bottom corner (default), a round badge, or split screen, lip-synced, block by block across the whole video (about 10 s and about 25 spoken words a block). The words are `script` (verbatim; refused free if it cannot fit) or, without one, written to fit from what the video says and shows (`brief` steers them). The video's own sound stays under the presenter unless baseAudio 'off'; the video without the presenter is kept. A face the video model refuses STOPS it with nothing charged: offer another creator, another photo, or the video as it is. dryRun:true = the exact price, which is what it charges. For a finished make_explainer film, make_explainer with fromFilm.
| Argument | Type | Description |
|---|---|---|
| look | string | |
| brief | string | without a script: what they should talk about |
| image | string | or a photo of the presenter (a real person's face follows the real-face rules) |
| scale | number | 0.2-0.8 of the frame |
| video* | string | the video: an https / upload_file URL, a Library id (L0…), or a local path on the CLI; up to 2 minutes |
| voice | string | a voice name, only when asked (default: their own voice) |
| dryRun | boolean | price only: nothing made, nothing charged |
| script | string | the exact words to say, used verbatim and split at sentence ends |
| creator | string | the presenter: a saved or preset creator, by name or id |
| captions | boolean | subtitles of what they say; only when asked |
| position | string | 'br' (default), 'bl', 'tr', 'tl' |
| baseAudio | string | the video's own sound: 'under' the presenter (default) or 'off' |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video"
],
"properties": {
"look": {
"enum": [
"corner",
"badge",
"split"
],
"type": "string"
},
"brief": {
"type": "string",
"description": "without a script: what they should talk about"
},
"image": {
"type": "string",
"description": "or a photo of the presenter (a real person's face follows the real-face rules)"
},
"scale": {
"type": "number",
"description": "0.2-0.8 of the frame"
},
"video": {
"type": "string",
"description": "the video: an https / upload_file URL, a Library id (L0…), or a local path on the CLI; up to 2 minutes"
},
"voice": {
"type": "string",
"description": "a voice name, only when asked (default: their own voice)"
},
"dryRun": {
"type": "boolean",
"description": "price only: nothing made, nothing charged"
},
"script": {
"type": "string",
"description": "the exact words to say, used verbatim and split at sentence ends"
},
"creator": {
"type": "string",
"description": "the presenter: a saved or preset creator, by name or id"
},
"captions": {
"type": "boolean",
"description": "subtitles of what they say; only when asked"
},
"position": {
"type": "string",
"description": "'br' (default), 'bl', 'tr', 'tl'"
},
"baseAudio": {
"enum": [
"under",
"off"
],
"type": "string",
"description": "the video's own sound: 'under' the presenter (default) or 'off'"
}
}
}Add a product to the brand's catalog from its product-page link: reads the page's own data (name, every photo, price and currency, description, its bullet-point selling points), no model, 0 credits. Re-adding a link updates the row (the live page wins). Without a url, `title` adds one by hand. `audience` and `sellingPoints` you pass are kept through later re-reads.
| Argument | Type | Description |
|---|---|---|
| url | string | the product page link |
| price | string | hand-added only; never printed as current unless read from a page |
| title | string | to add a product by hand (no link) |
| images | array | photo urls for a hand-added product |
| brandId | string | a profile id/name from list_brands; omit for the active profile |
| audience | string | who this product is for |
| currency | string | currency code for the price, e.g. USD |
| description | string | product description |
| sellingPoints |
Burn subtitles into ANY video and get the .srt too; nothing is cut or re-rendered. Set textStyle only when the user describes a look (default: white sentence case, thin outline, bottom). Timed per sentence, or auto:'words' for phrases on their spoken words. Only when asked, textStyle 'highlight' (or a highlightColor) lights the word being said, timed word by word. Or pass cues to burn your own lines exactly, untranscribed (reel-style phrases, *stage directions*). burn:false returns only the .srt. Takes a video file or URL, a YouTube/Vimeo-style page, or a TikTok / Reel / X post link. No speech is refused and refunded. Runs in the background and lands in the Library.
| Argument | Type | Description |
|---|---|---|
| auto | string | sentences (default) or words: timing unit for transcribed subtitles |
| burn | boolean | false = only the .srt |
| cues | array | your own lines in seconds, no overlap, max 90 chars, no emoji |
| video* | string | the video to subtitle; a URL, not a local file path; use upload_file for local files |
| textStyle | any | the look: words, a preset or fields; omit for the default |
| wordsPerCue | number | words per subtitle line when auto is words |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video"
],
"properties": {
"auto": {
"enum": [
"sentences",
"words"
],
"type": "string",
"description": "sentences (default) or words: timing unit for transcribed subtitles"
},
"burn": {
"type": "boolean",
"description": "false = only the .srt"
},
"cues": {
"type": "array",
"items": {
"type": "object",
"required": [
"text",
"start",
"end"
],
"properties": {
"end": {
"type": "number",
"description": "second the line disappears"
},
"text": {
"type": "string",
"description": "subtitle line text, max 90 characters, no emoji"
},
"start": {
"type": "number",
"description": "second the line appears"
}
}
},
"maxItems": 400,
"description": "your own lines in seconds, no overlap, max 90 chars, no emoji"
},
"video": {
"type": "string",
"description": "the video to subtitle; a URL, not a local file path; use upload_file for local files"
},
"textStyle": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"font": {
"type": "string",
"description": "sans|serif|elegant|condensed|hand or any Google Fonts family"
},
"size": {
"type": [
"string",
"number"
],
"description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction"
},
"tilt": {
"type": "number",
"description": "degrees, ±45"
},
"color": {
"type": "string",
"description": "any CSS colour"
},
"italic": {
"type": "boolean"
},
"preset": {
"type": "string"
},
"shadow": {
"type": "boolean"
},
"weight": {
"type": "number"
},
"outline": {
"type": "boolean"
},
"subFont": {
"type": "string"
},
"describe": {
"type": "string",
"description": "the look in words"
},
"position": {
"type": [
"string",
"number"
],
"description": "top|center|lower|bottom or 0.05-0.95 from the top"
},
"textCase": {
"enum": [
"as-is",
"upper",
"lower",
"title"
],
"type": "string"
},
"cardColor": {
"type": "string"
},
"subItalic": {
"type": "boolean"
},
"background": {
"type": "string",
"description": "none|pill|a colour"
},
"outlineColor": {
"type": "string"
},
"highlightColor": {
"type": "string",
"description": "word-timed subtitles only: the word being said is lit in this colour (preset highlight sets it)"
}
}
}
],
"description": "the look: words, a preset or fields; omit for the default"
},
"wordsPerCue": {
"type": "number",
"description": "words per subtitle line when auto is words"
}
}
}A video ad's structure: verbatim transcript (voiceover + on-screen text), beat list, duration and sampled frame times; study a reference before remixing it. ~A transcription call.
| Argument | Type | Description |
|---|---|---|
| url* | string | the video URL (a served /generated/ path or a public http(s) video); a URL, not a local file path; use upload_file for local files |
| words | any | true | 'only': each spoken word's start/end |
| frames | boolean | also return the frames as images |
| anchors | array | {afterWord|beforeWord|atWord, occurrence?, offset?} -> s |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "the video URL (a served /generated/ path or a public http(s) video); a URL, not a local file path; use upload_file for local files"
},
"words": {
"description": "true | 'only': each spoken word's start/end"
},
"frames": {
"type": "boolean",
"description": "also return the frames as images"
},
"anchors": {
"type": "array",
"items": {},
"description": "{afterWord|beforeWord|atWord, occurrence?, offset?} -> s"
}
}
}Append text to the end of a Google Doc Hermoso can reach — one it created (pass the documentId from create_doc) or one the user handed over with the Google file picker in the app (find its id with list_drive_files). Optional `dropdown:{title, options:[2-50], selected}` appends a Google Docs dropdown chip after the text — e.g. text "Status: " + dropdown {title:"Status", options:["Draft","In review","Approved"]} gives a brief a status the team can click to change; the chip is confirmed by reading the doc back. Set it later with update_doc dropdowns.
| Argument | Type | Description |
|---|---|---|
| text | string | text to append at the end of the doc (optional when a dropdown is passed) |
| dropdown | object | a dropdown chip to append after the text |
| documentId* | string | the document id from create_doc |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"documentId"
],
"properties": {
"text": {
"type": "string",
"description": "text to append at the end of the doc (optional when a dropdown is passed)"
},
"dropdown": {
"type": "object",
"required": [
"options"
],
"properties": {
"title": {
"type": "string",
"description": "the dropdown title, e.g. \"Status\""
},
"options": {
"type": "array",
"items": {
"type": "string"
},
"description": "2 to 50 distinct options"
},
"selected": {
"type": "string",
"description": "the option it starts on (default: the first)"
}
},
"description": "a dropdown chip to append after the text"
},
"documentId": {
"type": "string",
"description": "the document id from create_doc"
}
}
}Add to the END of an existing Notion page (pageId or its link, from list_notion_pages): markdown content, a swipefileCollection, or includeSchedule:true. Nothing already on the page changes. Free.
| Argument | Type | Description |
|---|---|---|
| pageId* | string | page id or link |
| content | string | markdown to add |
| includeSchedule | boolean | true appends the scheduled posts too |
| swipefileCollection | string | swipefile collection name or id to append, with each ad's creative, copy, dates and link |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"pageId"
],
"properties": {
"pageId": {
"type": "string",
"description": "page id or link"
},
"content": {
"type": "string",
"description": "markdown to add"
},
"includeSchedule": {
"type": "boolean",
"description": "true appends the scheduled posts too"
},
"swipefileCollection": {
"type": "string",
"description": "swipefile collection name or id to append, with each ad's creative, copy, dates and link"
}
}
}Append rows to a Google Sheet Hermoso can reach — one it created (pass the spreadsheetId from create_sheet) or one the user handed over with the Google file picker in the app (find its id with list_drive_files). rows = array of row arrays.
| Argument | Type | Description |
|---|---|---|
| rows* | array | rows to append — array of row arrays |
| range | string | range to append at (default A1 / first sheet) |
| spreadsheetId* | string | the spreadsheet id from create_sheet |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"spreadsheetId",
"rows"
],
"properties": {
"rows": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": [
"string",
"number",
"boolean"
]
}
},
"description": "rows to append — array of row arrays"
},
"range": {
"type": "string",
"description": "range to append at (default A1 / first sheet)"
},
"spreadsheetId": {
"type": "string",
"description": "the spreadsheet id from create_sheet"
}
}
}Import this brand's PAST posts from a channel into the performance record, so 'which hook works' can draw on history rather than only on what was published since Hermoso started recording. Supports facebook, instagram, threads, youtube, tiktok, pinterest and bluesky; the others say plainly why they cannot (LinkedIn has no enumerate-my-posts endpoint on our grant, X bills per read so it is excluded from bulk import, Google Business has had no per-post insights since 2023, and Telegram's Bot API cannot read a chat's past messages at all — nothing published before Hermoso is recoverable through a bot token). BOUNDED, RESUMABLE AND QUOTED: it runs as a DRY RUN by default and tells you how many posts it found and what reading them will cost — pass confirm:true to import, and pass the returned cursor to continue. AN IMPORTED POST IS WEAKER EVIDENCE THAN A RECORDED ONE and is labelled 'backfilled': its hook is recovered ONLY where the post matches a Hermoso creation by asset or caption. A post made outside Hermoso stays UNATTRIBUTED — it counts toward channel and format totals but never votes on which hook works. Never guess a hook from a caption. Free.
| Argument | Type | Description |
|---|---|---|
| limit | number | how many posts this page (default 50, max 200) |
| cursor | string | resume from a previous run |
| channel* | string | which channel to import from |
| confirm | boolean | actually import — omit for a dry run that only quotes the cost |
| accountRef | string | which Page / account, when the profile has more than one |
Show this account's billing at a glance: current plan (quote plan.priceUsd per plan.period), credit balance, whether auto-reload is on, whether a card is on file, and whether YOU (this key) have ADMIN rights to change billing. Read-only, free. Call it before upgrade_plan / set_auto_reload to know what's possible — members have read-only billing. IN A SHARED TEAM WORKSPACE A MEMBER SEES THE PLAN AND THE BALANCE ONLY: the workspace owner's payment card and auto-reload belong to them and are not reported (billingScope:'member'). Never tell a member there is no card on file — the honest answer is that you cannot see it. plan.memberRate: media models at cost + 10%; plan.legacy: an older plan kept at its price. posting: this month's posting allowance ("N of 10 posts used this month" on Free, unlimited on every paid plan).
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Out of credits? Top up with a credit PACK. Call with no argument to list the available packs (id · credits · price). If the account has a saved card and you have billing-admin rights, calling with `pack` quotes the exact charge and calling again with confirm:true AND the quote's quote_token charges the saved card instantly (same one-click top-up as the app — no redirect). If there's no saved card yet, you get a Stripe checkout URL to hand your human for the FIRST purchase; their card saves for one-click after that. Packs only; subscriptions are managed by a person in Billing. IF YOU ARE AN AGENT HOLDING YOUR OWN PAYMENT CREDENTIAL, there is a third path that needs no human at all: `POST /api/billing/machine-payment` with a `packId` answers HTTP 402 carrying an MPP challenge, and grants the pack once you authorise and retry with the credential — the same packs, the same prices, the same credits. `GET /api/billing/config` carries a `machinePayments` block listing the packs with their per-credit rates and saying whether that lane is enabled on this server. Most agents do NOT have their own credential yet, so the checkout link above remains the normal path. To stop running out entirely, turn on auto-reload with set_auto_reload (admin) — low balances then top themselves up from the saved card automatically.
| Argument | Type | Description |
|---|---|---|
| pack | string | the pack id to buy (e.g. pack-2k) — omit to list the available packs first |
| confirm | boolean | set true to actually charge the saved card for `pack` (required for the one-click charge; ignored on the checkout-link path) |
| quote_token | string | the quoteToken returned by the quote step — REQUIRED (with confirm:true) to charge; it binds the exact pack + price you quoted (10-minute validity) and makes a retried confirm idempotent |
Run ANY Hermoso tool by name — including the paid-campaign, analytics and channel-admin tools that are not in this session's starting list — with the same permissions, the same account and the same result as calling it directly. Get the exact `name` and its `args` from find_tools first. This is the route on hosts that cannot reload their tool list mid-conversation (claude.ai, ChatGPT): enable_tools switches a group on server-side, but such a host keeps the list it fetched at connect time. Arguments are validated against the tool's own schema and a mistake is answered with the expected parameters, not a silent default. Refused by name, with the way out, when the tool needs a connector this workspace has not made or is withheld by the host's own policy.
| Argument | Type | Description |
|---|---|---|
| args | object | the tool's arguments as an object, exactly as its own schema takes them |
| name* | string | name |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"args": {
"type": "object",
"description": "the tool's arguments as an object, exactly as its own schema takes them",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"name": {
"type": "string",
"description": "name"
}
}
}Swap the narration of a finished video into a different voice — keeps the performance, lip-sync, and background sound. Use when the user likes the video but wants a different narrator voice; use dub_video only for language translation. Paid; returns the served URL.
| Argument | Type | Description |
|---|---|---|
| video* | string | the source video URL; a URL, not a local file path; use upload_file for local files |
| voice | string | target narrator voice preset name, e.g. 'Aria', 'George', 'Rachel', 'Sarah', 'Brian', 'Charlotte' (defaults to a warm female read). A saved VOICE CLONE of the user's own voice counts as a preset here — name it the way it is saved on their cast |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video"
],
"properties": {
"video": {
"type": "string",
"description": "the source video URL; a URL, not a local file path; use upload_file for local files"
},
"voice": {
"type": "string",
"description": "target narrator voice preset name, e.g. 'Aria', 'George', 'Rachel', 'Sarah', 'Brian', 'Charlotte' (defaults to a warm female read). A saved VOICE CLONE of the user's own voice counts as a preset here — name it the way it is saved on their cast"
}
}
}Pre-flight ad copy against Meta's REAL, live Advertising Standards before you run it — a flat 1-credit check. Pulls Meta's actual policy pages and returns a verdict (pass / fix / block) where every flagged issue QUOTES Meta's own policy text verbatim plus a compliant rewrite that keeps the sell. It's a check, not an edit — it never changes the creative. Especially worth running for regulated-adjacent categories (health/supplements, weight-loss or beauty results claims, finance/crypto/insurance, alcohol, dating, gambling) or ANY strong/absolute/guaranteed claim.
| Argument | Type | Description |
|---|---|---|
| copy* | string | the ad copy / script / on-screen text to check |
| claims | string | the claims / proof points the ad makes |
| category | string | the product category — helps pick the relevant policy pages |
| imageDescription | string | a description of the creative / image when relevant |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"copy"
],
"properties": {
"copy": {
"type": "string",
"description": "the ad copy / script / on-screen text to check"
},
"claims": {
"type": "string",
"description": "the claims / proof points the ad makes"
},
"category": {
"type": "string",
"description": "the product category — helps pick the relevant policy pages"
},
"imageDescription": {
"type": "string",
"description": "a description of the creative / image when relevant"
}
}
}Check a finished video for named, mechanical issues, ONLY when the user asks (never after every render, and it never re-renders): loudness vs a target (default -14 LUFS) and true peak, black and frozen stretches, captions or text outside the platform-safe band, text running off the frame. Optional assertions about named elements: appearsBy {element, bySec}, before {a, b}, staysInFrame {element}, keepsMoving {maxStaticSec}. Each issue comes with a Fix line. The loudness, black and frozen checks are free; the on-screen text and element read is a small paid frame read (frames:false skips it).
| Argument | Type | Description |
|---|---|---|
| url* | string | the video: a /generated/ path, a public URL or a post link |
| frames | boolean | false skips the paid text and element read |
| assertions | array | what must hold, checked on sampled frames |
| loudnessTarget | number | integrated loudness target in LUFS, default -14 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "the video: a /generated/ path, a public URL or a post link"
},
"frames": {
"type": "boolean",
"description": "false skips the paid text and element read"
},
"assertions": {
"type": "array",
"items": {
"type": "object",
"required": [
"kind"
],
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "string"
},
"kind": {
"enum": [
"appearsBy",
"before",
"staysInFrame",
"keepsMoving"
],
"type": "string"
},
"bySec": {
"type": "number"
},
"element": {
"type": "string",
"description": "an element named in words, e.g. \"the logo\""
},
"maxStaticSec": {
"type": "number"
}
}
},
"maxItems": 12,
"description": "what must hold, checked on sampled frames"
},
"loudnessTarget": {
"type": "number",
"description": "integrated loudness target in LUFS, default -14"
}
}
}Empty a range of cells in a Google Sheet, leaving the rows themselves in place. DESTRUCTIVE: call it WITHOUT confirm first and nothing is cleared — you get back the real number of filled cells in that exact range. Show the user that number, get an unambiguous yes, then call again with confirm:true AND confirmCells set to it. The echo is not ceremony: it is what catches naming A1:Z1000 when you meant A1:Z10, which is the mistake that actually happens. There is deliberately NO default range. The clear is read back and reported as confirmed only if the range really is empty afterwards. To remove a whole tab instead, use manage_sheet_tabs.
| Argument | Type | Description |
|---|---|---|
| range* | string | the range to clear, e.g. "A2:D50" or "Sheet1!A2:D50" |
| confirm | boolean | true clears the range; call without it first to get the filled-cell count |
| sheetUrl | string | Google Sheets link, instead of spreadsheetId |
| confirmCells | number | echo back the filled-cell count the unconfirmed call reported |
| spreadsheetId | string | Google spreadsheet id; use this or sheetUrl |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"range"
],
"properties": {
"range": {
"type": "string",
"description": "the range to clear, e.g. \"A2:D50\" or \"Sheet1!A2:D50\""
},
"confirm": {
"type": "boolean",
"description": "true clears the range; call without it first to get the filled-cell count"
},
"sheetUrl": {
"type": "string",
"description": "Google Sheets link, instead of spreadsheetId"
},
"confirmCells": {
"type": "number",
"description": "echo back the filled-cell count the unconfirmed call reported"
},
"spreadsheetId": {
"type": "string",
"description": "Google spreadsheet id; use this or sheetUrl"
}
}
}Paste a product page link (Shopify, Amazon or any store page) and get a creator-style video ad planned for that product. Hermoso reads the page (name, photos, what it says about the product, price), saves the product to the brand's catalog, checks the page's real product photo and pins it to this ad (the brand's saved default photo is not changed), then plans the ad. A store that hides its page from servers is read in a real browser; failing that the name comes from the link and the photo from a search, and the reply says so. Nothing renders here: show the user the concept, then call render_ad with the returned creative (dryRun:true first for the exact price). Works with or without a saved brand.
| Argument | Type | Description |
|---|---|---|
| say | string | what the creator should say, or the angle, in the user's words |
| url* | string | the product page link, https://… |
| style | string | ugc_selfie (talking to camera, default), ugc_unboxing, ugc_demo (how-to), ugc_tryon (try-on or haul), ugc_product (product close-ups, no face), ugc_shopping (in-store find), ugc_delivery, transformation (before and after), day_in_life (at home), grwm (get ready with me), ugc_addiction (obsessed with it) |
| language | string | language for the script and copy, default English |
| durationSeconds | number | length in seconds, 4-180; default 15 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"say": {
"type": "string",
"description": "what the creator should say, or the angle, in the user's words"
},
"url": {
"type": "string",
"description": "the product page link, https://…"
},
"style": {
"enum": [
"ugc_selfie",
"ugc_unboxing",
"ugc_demo",
"ugc_tryon",
"ugc_product",
"ugc_shopping",
"ugc_delivery",
"transformation",
"day_in_life",
"grwm",
"ugc_addiction"
],
"type": "string",
"description": "ugc_selfie (talking to camera, default), ugc_unboxing, ugc_demo (how-to), ugc_tryon (try-on or haul), ugc_product (product close-ups, no face), ugc_shopping (in-store find), ugc_delivery, transformation (before and after), day_in_life (at home), grwm (get ready with me), ugc_addiction (obsessed with it)"
},
"language": {
"type": "string",
"description": "language for the script and copy, default English"
},
"durationSeconds": {
"type": "number",
"description": "length in seconds, 4-180; default 15"
}
}
}Cut ONE long video into several RANKED, ready-to-post short clips (podcast, webinar, interview, talk, long ad cut -> Reels/Shorts/TikTok). Transcribes with timestamps, picks the strongest SELF-CONTAINED moments, then cuts + reframes each with ffmpeg — no video model renders anything, so it is fast and cheap. THE VERTICAL REFRAME IS SUBJECT-AWARE: one cheap vision call per clip (billed as its own event) picks a SINGLE crop offset held for the whole clip, so a speaker sitting camera-left is not cropped out and the framing never drifts inside a clip; with nothing to discard or no single subject it stays dead centre — read `reframedToSubject` and each clip's `reframeWhy` back rather than assuming either way. ACCEPTS: a YouTube link (or Vimeo / Loom / Dailymotion / Streamable / Rumble / Wistia / Twitch / TED), a direct https .mp4/.mov/.webm, or a Hermoso /generated/ URL (upload_file turns a local file into one). NOT supported: TikTok / Instagram / Facebook links, and anything age-restricted, private, members-only, geo-blocked or still LIVE — those fail fast with the real reason and are fully refunded, so ask for a direct file or an upload rather than retrying. Source ~15s to ~600MB; only the first ~40 minutes is analysed (truncated:true says so). Cost: a ~7-credit hold settled to the exact transcription + encode cost, plus the clip-selection model's tokens as their own small event. RETURNS clips[] — each its OWN served mp4 URL, title, hook, ready-to-post caption, 0-100 score and source timecode. SUBTITLES ARE BURNED IN BY DEFAULT (slim white CAPS, thin black outline, bottom safe band, no box) because short-form is watched on mute; captions:false for clean footage. TIMING IS APPROXIMATE, NOT WORD-LEVEL — cues follow the transcript's per-sentence timestamps, split by character count; never promise frame-accurate sync. captionsBurned counts the clips that really carry a burned track and captionNote says why any are bare.
| Argument | Type | Description |
|---|---|---|
| count | number | how many clips to cut, 1-8 (default 4) |
| video* | string | the long video to clip — a YouTube/Vimeo/Loom/Dailymotion/Streamable/Rumble/Wistia/Twitch/TED watch URL, a direct https .mp4/.mov/.webm, or a Hermoso /generated/ URL; a URL, not a local file path; use upload_file for local files |
One-click STATIC-AD CLONE (the web app calls it Clone): rebuild a competitor/reference STATIC (image) ad as an on-brand version — SAME layout, composition and energy, but YOUR product, brand colours, logo and voice, with every trace of the source brand removed. Pass `imageUrl` = the static ad image to clone. Uses your saved brand (pass brandId to target a specific profile — that switches this key's active profile like use_brand). IMAGES ONLY — for a video ad use clone_video with its link, then render_ad. Bills as one image generation.
| Argument | Type | Description |
|---|---|---|
| brandId | string | a profile id/name from list_brands to clone for; omit to use the active profile |
| imageUrl* | string | the URL of the static ad image to clone. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"imageUrl"
],
"properties": {
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands to clone for; omit to use the active profile"
},
"imageUrl": {
"type": "string",
"description": "the URL of the static ad image to clone. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
}
}
}Remake a video you like FOR THIS BRAND from its link — a TikTok, Instagram Reel, Facebook video or reel, X post, YouTube Short or video, or a direct video file URL. Hermoso WATCHES it first (frames across the whole clip plus a transcript of the voiceover, on-screen text and cut map), then plans a storyboard that keeps its hook device, structure, jump cuts and pacing while swapping in THIS brand's product, cast, setting and words — never the original's words, face or brand. The new ad MATCHES THE ORIGINAL'S LENGTH (capped at 60s) unless durationSeconds is given. Renders nothing: pass the returned creative to render_ad to make the video. Costs the plan plus about 2 credits to read the link. The reply says exactly what was watched, and when a platform will not hand over the footage (YouTube sometimes refuses servers) it says the plan rests on the captions and thumbnail only. For a local file, upload_file it first and pass the URL.
| Argument | Type | Description |
|---|---|---|
| url* | string | the video to clone — a TikTok / Instagram Reel / Facebook / X / YouTube link, or a direct https video file URL; a URL, not a local file path; use upload_file for local files |
| brand | any | brand name or profile object; OMIT to use the workspace's saved brand (see get_brand) |
| changes | string | what to change or keep from the original, in the user's words (e.g. "same hook but in a gym", "keep the jump cut, older creator") |
| product | string | what the new ad sells, plus any angle or offer; omit to use the saved brand's product |
| language | string | language for the new ad's script and copy — default English |
Fetch fresh performance numbers for this brand's recorded posts and store them as a time-series. Metrics ACCRUE, so a post is read at ~24 hours and again at ~7 days; this collects whichever readings are due and skips the ones already taken. A channel that cannot report a metric records it as ABSENT with the reason — never as zero — and a read that fails is recorded as 'could not tell', which contributes to nothing. X IS SKIPPED BY DEFAULT because X bills us per API call: pass includeMetered:true to include it, and tell the user it costs credits BEFORE you do. The skip is always reported so a channel missing from the numbers is never mistaken for one that performed badly. Free except for X.
| Argument | Type | Description |
|---|---|---|
| max | number | cap how many posts to read in this run (default 40) |
| brand | string | WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done. |
| remeasure | boolean | ALSO re-read posts older than 7 days whose every reading came back empty or failed — use after post_performance reports posts "read but empty", or once a channel's reader has been fixed. Otherwise those windows stay closed. |
| includeMetered | boolean | also read X, which BILLS CREDITS per post read — ask the user first |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"max": {
"type": "number",
"description": "cap how many posts to read in this run (default 40)"
},
"brand": {
"type": "string",
"description": "WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done."
},
"remeasure": {
"type": "boolean",
"description": "ALSO re-read posts older than 7 days whose every reading came back empty or failed — use after post_performance reports posts \"read but empty\", or once a channel's reader has been fixed. Otherwise those windows stay closed."
},
"includeMetered": {
"type": "boolean",
"description": "also read X, which BILLS CREDITS per post read — ask the user first"
}
}
}Tear a competitor's ad strategy down into an actionable playbook: their opening-hook MIX, longest-running campaign THEMES, the WHITE SPACE nobody in their set runs, 2-3 render-ready COUNTER-PLAYS, and the territories they own that you should avoid. Pass `competitor` {name, domain?}. CONTRACT: supply `ads` (raw ad objects from a prior pull_competitor_ads / search_meta_ads call) to tear exactly those down, OR omit `ads` and this pulls the competitor's real Meta ads first (spends the same ad-library reads search_meta_ads does, longest-running = proven winners). Auto-tailors the white space + counter-plays to YOUR saved brand. Spends credits (free when you pass ads).
| Argument | Type | Description |
|---|---|---|
| ads | array | ad objects to tear down (from pull_competitor_ads / search_meta_ads). Omit to auto-pull their Meta ads first. |
| language | string | output language (default English) |
| competitor* | object | the competitor to tear down |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"competitor"
],
"properties": {
"ads": {
"type": "array",
"items": {
"type": "object",
"properties": {},
"additionalProperties": {}
},
"description": "ad objects to tear down (from pull_competitor_ads / search_meta_ads). Omit to auto-pull their Meta ads first."
},
"language": {
"type": "string",
"description": "output language (default English)"
},
"competitor": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "the competitor brand name"
},
"domain": {
"type": "string",
"description": "their domain — sharpens the auto-pull page match"
}
},
"description": "the competitor to tear down"
}
}
}Connect an account that links with a KEY rather than a sign-in screen, from here, with no browser: AppLovin Ads, Stripe, Intercom, Mailchimp, Klaviyo, Vercel, ChatGPT Ads, Apple Ads, Bluesky, Telegram, Bing Webmaster Tools, PostHog, Mixpanel, Amplitude, WordPress, Slack, Discord, Webhook. Pass provider and fields in that provider's own field names (listed below, * = required). The key is checked live with the provider before anything is saved, exactly as the app's Connectors page checks it, and the reply is read back from the saved connection. OFFER BOTH WAYS AND LET THE USER CHOOSE: a key pasted into this chat stays in this conversation's history, while pasting it in the app (Workspace > Connectors, or the one-click link https://app.hermoso.ai/?connect=<provider>) keeps it out of the chat. Hermoso never repeats a submitted key back. An account that connects through the provider's own sign-in screen (OAuth) cannot be connected here: this answers with the link to hand the user instead. Apple Ads with no key material first generates a signing key pair and returns the public key to register with Apple plus a setupToken to send back. Fields: applovin_ads {reportKey, eventKey, apiKey, capiKey, accountId} · stripe {apiKey*} · intercom {accessToken*, region} · mailchimp {apiKey*, server} · klaviyo {apiKey*} · vercel {token*, team} · openai_ads {apiKey*} · apple_ads {clientId, teamId, keyId, privateKey, setupToken, orgId} · bluesky {identifier*, appPassword*, pds} · telegram {token*} · bing_webmaster {apiKey*} · posthog {apiKey*, region, host, projectId} · mixpanel {username*, secret*, projectId*, region, workspaceId} · amplitude {apiKey*, secretKey*, region, host} · wordpress {siteUrl*, username*, appPassword*} · slack {webhookUrl, announceWebhookUrl} · discord {webhookUrl, announceWebhookUrl} · webhook {webhookUrl*}.
| Argument | Type | Description |
|---|---|---|
| fields | object | that provider's own field names and values, e.g. {"apiKey":"…"}; the names for each provider are in the description |
| provider |
Turn a file already in the user’s OneDrive into a PDF or a JPG — Microsoft does the conversion on its own servers, so nothing is re-encoded here and nothing is lost in a screenshot. It reads about 130 source formats, which is the point: PowerPoint and Word decks, Excel, Photoshop PSD, Illustrator AI, Sketch, 3D (fbx/glb/obj), video (mp4/mov/webm), HEIC from an iPhone, and the raw camera formats (CR2, NEF, ARW, DNG) that nothing else in this product can open. Use it to turn a client’s deck into images you can actually put in an ad, to get a usable JPG out of a designer’s PSD or a photographer’s raw file, or to hand someone a PDF of a spreadsheet. CONVERTING TO JPG REQUIRES BOTH width AND height — Microsoft refuses the call without them. The result is stored at a durable Hermoso URL you can pass straight to a render or a post; Microsoft’s own conversion link expires within minutes, so do not hand that one to anyone. Needs OneDrive connected — no new permission.
| Argument | Type | Description |
|---|---|---|
| width | number | REQUIRED for jpg — output width in pixels |
| fileId* | string | the OneDrive item id, from list_onedrive_files |
| format | string | default pdf |
| height | number | REQUIRED for jpg — output height in pixels |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"width": {
"type": "number",
"description": "REQUIRED for jpg — output width in pixels"
},
"fileId": {
"type": "string",
"description": "the OneDrive item id, from list_onedrive_files"
},
"format": {
"enum": [
"pdf",
"jpg"
],
"type": "string",
"description": "default pdf"
},
"height": {
"type": "number",
"description": "REQUIRED for jpg — output height in pixels"
}
}
}Add a NEW profile (a brand, client, creator or personal workspace) and switch to it. Each has its OWN brand details, memory, swipefile, Library, avatars, skills, playbooks and connectors; nothing leaks between them. Use draft_brand to FILL it and update_brand to edit it. Re-running with the same name returns the existing profile instead of a duplicate. Creating it is free. Drafting a NEW brand into it (draft_brand) needs the credits for its research on the balance, about 100; with fewer, draft_brand refuses before anything runs or is charged and says how many it needs and how many the account has.
| Argument | Type | Description |
|---|---|---|
| name* | string | the name for the new profile (a brand, client or creator name) |
| activate | boolean | switch this connection to the new profile (default true); everything you do next scopes to it |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "the name for the new profile (a brand, client or creator name)"
},
"activate": {
"type": "boolean",
"description": "switch this connection to the new profile (default true); everything you do next scopes to it"
}
}
}Create a new Google Doc in the user’s Drive with a title + optional body text — e.g. export ad copy, a creative brief, or a report. Returns the document id + URL. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets and Docs).
| Argument | Type | Description |
|---|---|---|
| text | string | body text to insert |
| title | string | document title |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"text": {
"type": "string",
"description": "body text to insert"
},
"title": {
"type": "string",
"description": "document title"
}
}
}Create a folder in the user’s Google Drive (optionally nested under parentId) to organize saved files. Returns the folder id + webViewLink. Use that ID as update_drive_file’s moveToFolderId or as parentId for a nested folder. NOTE: save_to_drive’s `folder` is a NAME, not this id — it find-or-creates a folder by that name, so pass the folder NAME there (or omit and just save, then move with update_drive_file).
| Argument | Type | Description |
|---|---|---|
| name* | string | folder name |
| parentId | string | parent folder id for a nested folder (default: Drive root) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "folder name"
},
"parentId": {
"type": "string",
"description": "parent folder id for a nested folder (default: Drive root)"
}
}
}Create a folder in the user’s OneDrive (optionally nested under parentId) to organize saved files. Returns the folder id + webViewLink. Use that id as update_onedrive_file’s moveToFolderId or as parentId for a nested folder. NOTE: save_to_onedrive’s `folder` is a NAME (find-or-created), not this id.
| Argument | Type | Description |
|---|---|---|
| name* | string | folder name |
| parentId | string | parent folder id for a nested folder (default: OneDrive root) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "folder name"
},
"parentId": {
"type": "string",
"description": "parent folder id for a nested folder (default: OneDrive root)"
}
}
}Create a new Google Spreadsheet in the user’s Drive and optionally fill it with rows — e.g. export a swipefile, ad list, or performance report. Pass rows as an array of row arrays (first row = headers). Returns the spreadsheet id + URL. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets and Docs).
| Argument | Type | Description |
|---|---|---|
| rows | array | rows to write — array of row arrays; first row = headers |
| title | string | spreadsheet title |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"rows": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": [
"string",
"number",
"boolean"
]
}
},
"description": "rows to write — array of row arrays; first row = headers"
},
"title": {
"type": "string",
"description": "spreadsheet title"
}
}
}PERMANENTLY delete a profile and EVERYTHING in it — brand details, memory, swipefile, Library creations, generated assets, avatars, skills, playbooks, chats — and disconnect its connected accounts. Irreversible, and it applies to everyone the workspace is shared with. Call it WITHOUT confirm first: it reports exactly what that workspace holds. Show the user that inventory verbatim, get an unambiguous yes, then call again with confirm:true — plus, if the workspace is not empty, confirmName set to its exact name and confirmConnectors set to the number of connected accounts it reported. Those two exist because confirming INTENT does not prove you picked the right WORKSPACE, and a wrong target is how a live profile was destroyed. The account's FIRST/anchor profile cannot be deleted this way (it holds the workspace's root storage) — that one is replaced from the app.
| Argument | Type | Description |
|---|---|---|
| brand* | string | profile id or exact name from list_brands |
| confirm | boolean | REQUIRED true — this destroys the whole profile and cannot be undone |
| confirmName | string | the workspace's EXACT name, required when it is not empty — copy it from the inventory this tool returned, after the user has agreed to it |
| confirmConnectors | number | the number of connected accounts the inventory reported, required when there is at least one — the user must specifically agree to losing them, because reconnecting each needs a browser and no agent can do it |
Remove a saved creator from this workspace’s cast by id (from list_creators). Records a cross-device delete so they don’t reappear on the user’s other devices. It only drops the roster entry — ads already rendered with that person are untouched — and the same portrait can be saved again with save_creator, so no confirm is needed.
| Argument | Type | Description |
|---|---|---|
| id* | string | the creator id (from list_creators) |
| brand | string | profile id/name from list_brands, this call only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the creator id (from list_creators)"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
}
}
}Delete a Drive file. By default it goes to Trash (recoverable); pass permanent:true to delete it forever. Pass fileId (from list_drive_files) + confirm:true. Irreversible when permanent — confirm with the user first.
| Argument | Type | Description |
|---|---|---|
| fileId* | string | the Drive file id |
| confirm | boolean | REQUIRED true |
| permanent | boolean | true = delete forever; default trashes (recoverable) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"fileId": {
"type": "string",
"description": "the Drive file id"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
},
"permanent": {
"type": "boolean",
"description": "true = delete forever; default trashes (recoverable)"
}
}
}PERMANENTLY delete images/videos from this brand's Library: the entry and the stored file. Pass ids (L0… from list_library) or urls. Without confirm it lists what would be deleted; ask the user, then call again with confirm:true. Free.
| Argument | Type | Description |
|---|---|---|
| ids | array | Library ids from list_library |
| urls | array | file links |
| confirm | boolean | true deletes |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "Library ids from list_library"
},
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "file links"
},
"confirm": {
"type": "boolean",
"description": "true deletes"
}
}
}Delete a OneDrive item — it moves to the OneDrive recycle bin (recoverable there). Pass fileId (from list_onedrive_files) + confirm:true. Confirm the exact file with the user first.
| Argument | Type | Description |
|---|---|---|
| fileId* | string | the OneDrive item id |
| confirm | boolean | REQUIRED true |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"fileId": {
"type": "string",
"description": "the OneDrive item id"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
}
}
}Delete a saved playbook by id (from list_playbooks). Records a cross-device delete so it does not come back on the next sync. Minor + re-creatable, so no confirm needed.
| Argument | Type | Description |
|---|---|---|
| id* | string | the playbook id (from list_playbooks) |
| brand | string | profile id/name from list_brands, this call only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the playbook id (from list_playbooks)"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
}
}
}Delete one of the workspace’s CUSTOM skills by id (from list_skills). Built-in skills/recipes can’t be deleted. Minor + re-creatable, so no confirm needed.
| Argument | Type | Description |
|---|---|---|
| id* | string | the custom skill id (from list_skills) |
| brand | string | profile id/name from list_brands, this call only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the custom skill id (from list_skills)"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
}
}
}WHAT TO FIX NEXT, post by post — and the tool that fixes it. post_performance tells you which hook is AHEAD; this tells you what is WRONG with a given post and where the next edit goes. Under-distributed is A HOOK PROBLEM (change the opening: mine_angles, then list_hooks, then plan_variations). Seen but not held is A RETENTION PROBLEM (plan_variations to rebuild the middle against the same hook). Seen, held, and still not converting is AN OFFER PROBLEM. FOUR REFUSALS, AND YOU SHOULD REPEAT THEM RATHER THAN PAPER OVER THEM: (1) a post younger than ~24h is TOO EARLY and is never called a failure — it has not had its run; (2) a metric the platform does not publish is UNMEASURED, never zero — Facebook has published no post reach since 2026-06-15, Reddit publishes no impressions, and Google Business publishes nothing per-post at all; (3) below 5 measured posts on a channel there is no baseline of the brand's own, and the ONLY fallback is a published short-video hook floor that is NOT our measured number and does not transfer off TikTok/Instagram/YouTube — it is attributed in the output and you should attribute it too; (4) it does not always find a problem, and 'nothing here needs fixing' is a real answer rather than a failure to look. Hermoso cannot see conversions for an organic post — no channel reports installs or purchases against a post id — so the offer rung runs ONLY when the user tells you they are not converting and you pass converting:false. Print `summary` verbatim. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| limit | number | how many recent posts to diagnose (default 25, max 200). The baseline is always built from EVERY post recorded for the brand, never only these, so a bad month can never become its own definition of normal. |
| channel | string | restrict to one channel: facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky |
Disconnect a third-party account from this workspace (Meta, Google Ads, Google Drive/Sheets/Docs, YouTube, TikTok, LinkedIn, X, Reddit Ads, Pinterest, Google Business, Microsoft Advertising/OneDrive, Slack, …). This always drops the stored credentials, so every tool for that provider stops working immediately and posts/campaigns already published are NOT affected. IT NEVER REVOKES THE GRANT AT THE PROVIDER: the authorisation stays in place, so the same account keeps working in any other brand it is connected to, until the user removes Hermoso in that provider's own settings. The unconfirmed call names where to do that (list_connectors carries revokesAtProvider:"local") — relay it rather than promising a revoke. RECONNECTING NEEDS A BROWSER (the provider's consent screen) — an agent cannot undo this. Name the provider to the user, then call with confirm:true. Use list_connectors for the exact provider ids.
| Argument | Type | Description |
|---|---|---|
| account | string | on a channel with several connected accounts (TikTok, X, YouTube, Threads, Bluesky, Telegram, Pinterest): remove ONLY this account (@handle or id from list_connector_accounts) and keep the others |
| confirm | boolean | REQUIRED true — reconnecting a sign-in account afterwards needs the user's browser; a paste-a-key account is reconnected with connect_connector |
| provider* | string | provider id exactly as list_connectors reports it, e.g. "meta", "google_ads", "youtube", "linkedin" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider"
],
"properties": {
"account": {
"type": "string",
"description": "on a channel with several connected accounts (TikTok, X, YouTube, Threads, Bluesky, Telegram, Pinterest): remove ONLY this account (@handle or id from list_connector_accounts) and keep the others"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — reconnecting a sign-in account afterwards needs the user's browser; a paste-a-key account is reconnected with connect_connector"
},
"provider": {
"type": "string",
"description": "provider id exactly as list_connectors reports it, e.g. \"meta\", \"google_ads\", \"youtube\", \"linkedin\""
}
}
}Fill in a profile (a business brand or a creator) from a website domain, a free-text description, or a social handle, into a {name, products, logo, …} object you can pass to plan_ad / generate. IMPORTANT: a domain can resolve to a DIFFERENT company than intended (e.g. bala.com is an engineering firm, not the Bala fitness brand at shopbala.com). Before spending any credits on research or renders, VERIFY the returned `name` (and `summary`) match the brand the user meant; if it looks wrong, re-draft with the correct domain or a description (pass save:false until confirmed) — this tool cannot ask the user, so the caller owns that check.
| Argument | Type | Description |
|---|---|---|
| save | boolean | save onto the active profile (like Studio onboarding) so plan_ad/create use it automatically. Default: saves only when NO brand is saved yet; pass true to REPLACE the saved profile (the drafted fields overwrite the saved ones), false to never save |
| domain | string | a website to scrape, or a social link (profile, YouTube channel or one post) to draft from the account it names |
| platform | string | socialHandle platform: instagram/tiktok/youtube/twitter/threads |
| confirmed | boolean | user confirmed it is their brand: read + save site product photos (paid) |
| description | string | a free-text brand description (no website) |
| socialHandle | string |
Localize a finished video into another language WITHOUT re-rendering it: each spoken line is said where it was said, in a native voice, lip-synced, background kept. Pass the video and the language; the script is read off the source (`script` only overrides it). Paid by length; dryRun quotes. Returns the localized video URL.
| Argument | Type | Description |
|---|---|---|
| video* | string | the source video URL; a URL, not a local file path; use upload_file for local files |
| voice | string | optional, e.g. 'Aria' (woman) or 'George' (man): a native voice of that sex speaks. Default: the source speaker's sex. |
| dryRun | boolean | quote only |
| script | string | OPTIONAL override for the original spoken words. Leave this out — the source video is transcribed automatically. Only pass it when you already know the exact script and the auto-transcript got it wrong. |
| language* | string | target language, e.g. 'Spanish', 'de', 'French (Canada)' |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video",
"language"
],
"properties": {
"video": {
"type": "string",
"description": "the source video URL; a URL, not a local file path; use upload_file for local files"
},
"voice": {
"type": "string",
"description": "optional, e.g. 'Aria' (woman) or 'George' (man): a native voice of that sex speaks. Default: the source speaker's sex."
},
"dryRun": {
"type": "boolean",
"description": "quote only"
},
"script": {
"type": "string",
"description": "OPTIONAL override for the original spoken words. Leave this out — the source video is transcribed automatically. Only pass it when you already know the exact script and the auto-transcript got it wrong."
},
"language": {
"type": "string",
"description": "target language, e.g. 'Spanish', 'de', 'French (Canada)'"
}
}
}EDIT an existing image in place with a plain-language instruction and keep everything else: 'make the headline bigger', 'add our logo bottom right', 'swap the background for a kitchen', 'remove the person on the left', 'erase all the text'. Pass `image` (URL, Library item, upload_file URL or local path) and `instruction`. The same edit the web Studio's Edit runs: composition, aspect ratio, people and every untouched line of text stay as they are; the saved brand's real name and website are pinned so an added line never invents one, and the brand's real logo FILE is put on when the instruction asks for the logo (never redrawn by the model): an ask only about the logo ('the logo is wrong', 'make the logo bigger'; or logo:true, any language) runs no image model. Set removal:true when the edit STRIPS text, branding or an object, so nothing branded is put back. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. For a precise region, pass `mask` (see generate_image). One image edit's credits; returns the new image URL. For a new image from a prompt use generate_image; to rebuild a competitor's ad for your brand use clone_static.
| Argument | Type | Description |
|---|---|---|
| logo | boolean | |
| mask | string | optional mask image (URL or local path) marking the region to change: transparent = change, or white = change on an opaque mask |
| image* | string | the image to edit: URL, Library item URL, upload_file URL or local path |
| model | string | the editor, only when the user names one: gpt-image-2.6, gpt-image-2.6-flare (its own faster editor) or gpt-image-2.5. Omit for the default |
Compose ANY edit or transition yourself; there is no preset list. Segments on an output timeline (later ones drawn on top; overlapping ones ARE the transition), each animated by keyframes, plus your own HTML overlays and masks, compiled server-side into one render. Whip pan, zoom through, push, spin, speed ramp, slow motion, freeze frame, reverse, flash, J-cut / L-cut, circle or shape wipe, split screen, picture in picture: all composed from the fields below. Local render, the flat post_edit price (~2 credits, overlays included); a `generate` segment (a paid generated transition shot) only when the user explicitly asks for one. LOOK FIRST AND AFTER: video_frames(url, start, end, fps) shows exact frames free, so find the moment (the second the egg tips) before you cut, and check the seam of what you rendered before you call it done. SOURCES: src is 'this' (videoUrl) or any Library / render URL, direct file or PUBLIC TikTok / Reel / Facebook / X / YouTube post link (a viral hook's link works as is). in/out are SOURCE seconds; out:'payoff' cuts just before the hook's payoff, found from its motion. AUDIO: audio.tail:'payoff' carries the hook's OWN payoff sound across the cut (the egg's real splat over our clip); tail seconds = an L-cut; lead = a J-cut; gain dB or keys; mute. A VIRAL HOOK + THEIR PRODUCT: start from the hook. A clip matched to an unrelated hook never reads as one video, so write the clip AFTER the hook for it: a linking script + shot brief (the first line answers the hook, e.g. 'still waiting for the egg to land... anyway, come check out our restaurant'; what to film so it follows on; 5-15 s; then the pitch). The user records it, or you generate it (render_ad / generate_video, cost quoted first, on their OK); then join here: the hook with out:'payoff' and audio.tail:'payoff', then their clip. Match an existing unrelated clip only if they insist. A {generate:{prompt, seconds 3-8}} segment (a generated transition-only shot, paid, postEditTimeline) is never suggested; build it only when they explicitly ask for one. LINK FIRST: an effect alone never connects two unrelated clips; the link comes from what is in the frames. (1) Match cut, the default: video_frames (with its MOTION readout) on the hook's last second and across the other clip; pick the out-point AND the in-point (in: seconds, not always 0) where a motion direction, a screen position or size, a shape, a surface, a gesture or a gaze carries across, then ride the effect on that shared motion. (2) Its host names the hook in the first line. If the two share nothing, say so and offer the follow clip made for the hook. PRO, NOT IMOVIE: ease every curve (never linear on a move); keep the picture filling the frame through a move (scale up while it moves: two frames sliding side by side with a seam is the amateur tell); hide the handoff under the fastest, blurriest frames; carry direction into the next shot; cut on motion; end every effect cleanly; 0.2-0.6 s in total; a sound whose peak lands on the handoff (sfx whoosh at handoff minus 0.45 s, or the hook's own payoff sound). Moving segments get a real shutter blur automatically (motionBlur). EVERY SEAM IS MATCHED AUTOMATICALLY, hard cuts too: each cut is measured and the incoming clip graded (exposure, white balance, black level), grained UP (never smoothed) and softened while it moves toward the outgoing one; the reply gives before/after deltas per seam. match (timeline: every cut; segment: the cut into it): 'auto' default, 'off' for a deliberate contrast, or {grade, level, grain, blur: false to skip one, strength 0-1}; a segment's own constant exposure / contrast / saturation replaces the automatic grade. EVERY STITCH CHANGES THE SHOT: where two different clips (or two non-contiguous moments of one) meet at the same framing, the incoming one gets a simple crop of the same footage, varied: zoomed in on the subject, back out to the wide, or reframed off-centre (reframe: 'off' or the step 1.1-1.5; a segment with its own framing, a card or a transition is left alone). Still yours: subject size and headroom (scale it, never a jump from a third of the frame to two thirds) and sound (a 0.25-0.5 s J/L-cut, never a sonic wall). A clip placed after a hook gets a BUDGET (total - intro = its surviving window, and what was dropped). The plainest thing that links wins: a straight cut on action beats a decorative effect; over 0.5 s is too long in anything under 20 s; never flash more than 3 times a second. RECIPES (c = the cut second, adapt freely): whip pan: A over its last 0.22 s x 0 to -0.22, scale 1 to 1.35, mblur 0 to 220, all ease in; B overlap 0.08, opacity 0 to 1 over 0.08, x 0.22 to 0, scale 1.35 to 1, mblur 220 to 0, all ease out over 0.3 s; whoosh at c-0.45. Zoom through: A over its last 0.35 s scale 1 to 3 ease in anchored on the object, blur 0 to 10; B overlap 0.12, opacity 0 to 1, scale 1.5 to 1 and blur 10 to 0 ease out over 0.4 s. Cut on action: A out ON the motion, B scale 1.08 to 1 ease out over 0.25 s, audio.lead 0.2. Speed ramp: speed [{src:t0,v:1},{src:t0+0.25,v:0.3}] then [{src:t1,v:0.3},{src:t1+0.1,v:2}] into the cut. Circle wipe: B overlap 0.5 + overlays [{mode:'mask', segment:1, start, end, html: a white div whose clip-path circle grows via @keyframes}]. Card (picture in picture: a proven ad playing in a rounded card over the host watching it, any length): the host segment full frame, then the clip ON TOP with at:0, fit:'contain' (crop to reframe it), scale ~0.6-0.7, y ~0.12, radius ~0.04-0.06; a card is not a cut, so it is never graded toward the host; duck it under the host's first line with audio.gain keys (the host's voice leads the switch) and end it on a hard cut at a sentence break. SELF-CRITIQUE: the reply carries a vision REVIEW of each seam (pro / ok / amateur, linked or not, with fixes; about 2 credits, review:false skips it) and the seam frames. When it says ok or amateur, fix what it names and re-run the same sources (twice at most) before presenting; on a re-run give a generated segment {src: its URL, between: true} so it is not paid for twice. Then look at the WHOLE result once with video_frames, not only the seams: the first frame is not black or frozen, no dead air over ~0.3 s at the head, no lone black, flash or repeated frame at a cut, and nothing static for more than ~4-5 s (recut it or add a re-hook). FOLLOW-UPS ('cut earlier', 'no splat', 'whip pan instead', 'use the second hook') re-run this with the SAME sources and the one change; the result echoes the resolved timeline (e.g. the found payoff cut) to edit from. Refusals are free and name the field.
SWAP AN OBJECT IN A VIDEO ('replace the cup with our can' + the product photo as elements) or EDIT an existing clip from a plain instruction (video-to-video): the motion, timing, framing and cut stay, the named thing changes. 'change only the mug to red', 'make it nighttime', 'restyle it as claymation'. Your words are wrapped so the model keeps everything else identical, changes only what you named and repeats that lock; lighting is kept unless the change needs new light (set lighting to force either); literal:true sends your words as written. One change per call holds best. previewFirstFrame:true edits ONE still first (one image edit; previewAt picks the second) and quotes the clip; nothing else runs until you call again, ideally with previewStill. The reply scores how well the shot held outside the change (free) and flags an edit that touched more than asked. NOT for cuts/trims/end cards (post_edit), a new video (generate_video / render_ad), translating what is SPOKEN (dub_video) or a saved creator's face (recast_motion). Translating or rewriting the on-screen words of a video laid out from a design (a template ad, a custom design) is textChanges with every line at once: one re-make of the design, about 2 credits. Best on 3-15s clips.
| Argument | Type | Description |
|---|---|---|
| video* | string | the source video URL (a render, job result or list_library); a URL, not a local file path; use upload_file for local files |
| engine | string | default auto: Seedance 2.5 Edit without a real-looking person, else Kling 4.0 Edit |
| literal | boolean | true = send the instruction exactly as written, with no preserve/lock wrapper |
| elements | array | OPTIONAL identity/product grounding (≤4): a creator portrait or the real product photo, so the edit restores the REAL thing. Describe each one in the instruction |
LIST a group of tools that is not in this session's roster. IT IS NOT HOW YOU REACH A TOOL — call_tool runs any Hermoso tool whether or not it is listed, and that works everywhere. Use this when the session will use MANY tools from one area and you want them in your list. WORKS ON CLIENTS THAT RE-READ THE TOOL LIST (stdio, the CLI, Cursor, Claude Code); a host that fixed its roster at connect time — claude.ai and ChatGPT do — will not show the new tools until it reconnects, and this tool says so in its reply rather than reporting a success you cannot use. The connect-time route that always works is `?tools=all` on the server URL. The default roster is CORE-FIRST: the core tools plus a few that make the connection drivable. Every other tool is held out of the LIST on SIZE alone — the whole registry is several hundred thousand tokens of schema re-sent on every turn, and a roster far past the 30-50 tool mark measurably degrades tool choice. The heaviest groups are `ads`, `analytics`, `channel_admin`: paid-campaign management across every ad platform is most of the total schema weight. NOTHING held out is unfinished or unsafe, and nothing is unreachable — find_tools finds it and call_tool runs it. CALL THIS WHEN A WHOLE AREA IS IN PLAY. If the user settles into building, budgeting, targeting or reporting on ad campaigns, call enable_tools({groups:['ads']}) and the tools appear. If they ask about their own site or product analytics, a tag/tracking container, or how a search engine crawls, indexes or ranks their site, call enable_tools({groups:['analytics']}). Groups: core, research, create, channels, channel_admin, analytics, ads, files, workspace — or 'all'. Free, instant, and it never turns anything off.
| Argument | Type | Description |
|---|---|---|
| groups* | array | Groups to switch on, e.g. ['ads']. Unknown names are refused by name rather than ignored. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"groups"
],
"properties": {
"groups": {
"type": "array",
"items": {
"type": "string"
},
"description": "Groups to switch on, e.g. ['ads']. Unknown names are refused by name rather than ignored."
}
}
}One error group in full by fingerprint (from list_errors): every field, plus the most recent redacted occurrences — status, connector, job id, workspace, and a shape-only echo of the inputs. This is what makes a bug reproducible. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| fingerprint* | string | the `fp` value from list_errors |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fingerprint"
],
"properties": {
"fingerprint": {
"type": "string",
"description": "the `fp` value from list_errors"
}
}
}Export the posting schedule to a Notion database, one row per post: date, channels, caption, media link and status. Creates a new database (under parentPageId, or private in the workspace) or writes into an existing one (databaseId), matching its columns by name. historyDays adds posts already published. Up to 100 rows per run; the answer says if more were left. Free.
| Argument | Type | Description |
|---|---|---|
| title | string | name for a new database |
| databaseId | string | an existing database id or link |
| historyDays | number | 0-90 days of published posts to include |
| dataSourceId | string | data source id of an existing database to write rows into, matched to columns by name |
| parentPageId | string | Notion page to create the new database under; omit for a private page in the workspace |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"title": {
"type": "string",
"description": "name for a new database"
},
"databaseId": {
"type": "string",
"description": "an existing database id or link"
},
"historyDays": {
"type": "number",
"description": "0-90 days of published posts to include"
},
"dataSourceId": {
"type": "string",
"description": "data source id of an existing database to write rows into, matched to columns by name"
},
"parentPageId": {
"type": "string",
"description": "Notion page to create the new database under; omit for a private page in the workspace"
}
}
}Turn a SWIPEFILE COLLECTION into a real Google Slides deck — one slide per saved ad, carrying the creative, the brand, the ad copy, the run dates with the run length, and the platform. This is the thing a marketer actually presents to a client or a team. Returns the presentation id + URL. Creates a NEW deck every time: under the drive.file scope Hermoso can only touch files it created, so it cannot add slides to a deck the user already has. A creative whose ad-library link has expired cannot be embedded — Meta signs those URLs with a short expiry — so that slide says so in words and keeps its copy and run dates, and the reply reports how many. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets, Docs and Slides).
| Argument | Type | Description |
|---|---|---|
| limit | number | max ads to include, 1-60 (default 30) |
| title | string | deck title (default: the collection name) |
| collection | string | the swipefile collection to export, by name or id (default: the first collection) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max ads to include, 1-60 (default 30)"
},
"title": {
"type": "string",
"description": "deck title (default: the collection name)"
},
"collection": {
"type": "string",
"description": "the swipefile collection to export, by name or id (default: the first collection)"
}
}
}Before any paid render: does a picture or video contain a face, and which video models refuse it (they cannot take a real person's face; a render there fails after it starts) or were measured to take one, plus the model a face-bearing render is routed to. A local detector, no model call, 0 credits. It cannot tell a photo from a drawing, so a drawn face counts. Takes a picture or video URL, a Library item or a public post link. A file or Library item is free; a TikTok / Reel / Facebook / X post link is opened through one paid lookup (1 credit), refused when the account has no credits.
| Argument | Type | Description |
|---|---|---|
| url* | string | the picture or video to check |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "the picture or video to check"
}
}
}Pull an APP brand's REAL store screenshots (Google Play or App Store) into the workspace brand, so a screen-hungry native format can use them. Use when the brand has 0–1 app screens on file and you want make_template_ad(template:'app-ui-tour'), or the user asks to 'pull my app's screenshots'. A store link or package id goes in storeUrl and is read exactly; else pass appName (defaults to the saved brand's name). A web app with no store listing gets the screenshots of the product its OWN website shows (pictures the site labels as its interface). Call it before telling the user you have no screenshots of their app. FREE — a keyless store lookup or one read of the brand's homepage. It needs a CONFIDENT match: an ambiguous or unknown app returns 0 screens and saves nothing, which you should relay plainly rather than retrying with guesses. On success the screens are saved to the brand (durable URLs) and are immediately usable.
| Argument | Type | Description |
|---|---|---|
| appName | string | the app's name to look up on the App Store — defaults to the saved brand's name |
| brandId | string | a profile id/name from list_brands to save the screens onto; omit to use the active profile |
| storeUrl | string | a Google Play / App Store link or package id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"appName": {
"type": "string",
"description": "the app's name to look up on the App Store — defaults to the saved brand's name"
},
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands to save the screens onto; omit to use the active profile"
},
"storeUrl": {
"type": "string",
"description": "a Google Play / App Store link or package id"
}
}
}Resolve a generated asset reference (a /generated/… path or any URL) to a clickable absolute URL + a direct download URL.
| Argument | Type | Description |
|---|---|---|
| url* | string | the asset url or /generated/ path |
| name | string | optional filename for the download |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "the asset url or /generated/ path"
},
"name": {
"type": "string",
"description": "optional filename for the download"
}
}
}Generic escape hatch for any ALLOWLISTED long-tail social/web endpoint the dedicated search_* tools don't cover — e.g. {path:'/v1/instagram/profile', params:{handle:'nike'}}. Allowlisted platform families: TikTok (+ TikTok Shop), Instagram, YouTube, Facebook (organic profiles/posts/events/marketplace), LinkedIn (organic posts/companies), Twitter/X, Reddit, Threads, Snapchat, Pinterest, Twitch, Bluesky, Truth Social, Rumble, Spotify, SoundCloud, GitHub, Google search, link-in-bio pages (Linktree etc.). Param names vary per endpoint (profiles use `handle`, keyword searches use `query`, Reddit uses `subreddit`). WARNING: returns RAW provider JSON — large and messy; prefer the dedicated search_* tools. Spends credits.
| Argument | Type | Description |
|---|---|---|
| path* | string | exact endpoint path, e.g. '/v1/tiktok/profile' — non-allowlisted paths are rejected |
| params | object | endpoint query params, e.g. {handle:'nike'} |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"path"
],
"properties": {
"path": {
"type": "string",
"description": "exact endpoint path, e.g. '/v1/tiktok/profile' — non-allowlisted paths are rejected"
},
"params": {
"type": "object",
"properties": {},
"description": "endpoint query params, e.g. {handle:'nike'}",
"additionalProperties": {}
}
}
}Discover a brand's competitor / similar / adjacent brands from its domain (Claude grounded by web search). mode=competitors (default, excludes the searched company), inspiration (best relevant ads incl. it), or company. Bills the discovery model's token cost (no ad-data charge); free inside a new account's first-brand setup.
| Argument | Type | Description |
|---|---|---|
| mode | string | 'competitors' (default, excludes the searched company), 'inspiration' (best relevant ads incl. it), or 'company' |
| domain* | string | the brand domain, e.g. flourish.com |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"domain"
],
"properties": {
"mode": {
"enum": [
"competitors",
"inspiration",
"company"
],
"type": "string",
"description": "'competitors' (default, excludes the searched company), 'inspiration' (best relevant ads incl. it), or 'company'"
},
"domain": {
"type": "string",
"description": "the brand domain, e.g. flourish.com"
}
}
}Scan organic TikTok, Instagram Reels and YouTube for a niche across a few query variants, fold the posts into creators, and rank them on median views, engagement rate and how often they show up for that niche; the top rows get follower counts AND public contact info (an Instagram business email / phone / category, the bio link, an email in a TikTok bio) so outreach can start from the result. Real people, not AI actors — for influencer sourcing, UGC casting and partnership prospecting ("who should we send product to?"). About one credit per search call (platforms × queries, default 3 × 3) plus one per enriched profile; repeats inside 20 minutes are free. Then shortlist (save_to_swipefile), check a profile, draft outreach (generate_text), or approve them for Partnership Ads. Marketplace:true also searches Instagram’s creator marketplace (Meta’s own creator directory: followers, badges, marketplace email) for the same niche and returns those rows beside the ranked list.
| Argument | Type | Description |
|---|---|---|
| limit | number | creators to return, 1–30 (default 12) |
| niche* | string | product category, topic or hashtag — "calorie tracker app", "matcha", "#cleanbeauty" |
| enrich | boolean | followers + public contact for the top 3 on TikTok and on Instagram (default true, 1 credit each) |
| queries | number | query variants per platform, 1–4 (default 3); each is a paid search call |
| platforms | array | default all three |
| marketplace | boolean |
A sound for an edit by name ('the FAAAA sound'), by the moment ('bad news reaction') or by link (TikTok sound, meme-sound page, post, audio file): a durable mp3 and where it starts and lands. Named/described sounds come from what TikTok uses now; pick takes another candidate. A sound that does not exist yet is generate_sfx.
| Argument | Type | Description |
|---|---|---|
| url | string | link to a TikTok sound, a post, a meme-sound page or an audio file |
| pick | number | candidate number from the previous list to take instead of the first |
| query | string | sound by name, moment or meaning, e.g. 'bad news reaction' |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "link to a TikTok sound, a post, a meme-sound page or an audio file"
},
"pick": {
"type": "number",
"description": "candidate number from the previous list to take instead of the first"
},
"query": {
"type": "string",
"description": "sound by name, moment or meaning, e.g. 'bad news reaction'"
}
}
}Search EVERY Hermoso tool — your starting list is deliberately short, and everything else in the product is here — by name, task or group. Each row gives the tool's PARAMETERS in one line, its CREDIT COST (free means free on every plan; a tool that runs a model quotes the live per-model figure) and its recent HEALTH on this server (failure rate and typical duration, or "no recent calls", which means unseen and not broken). Use it the moment the user asks for something you do not see a tool for (a campaign, an ad set, a lead form, a click-to-WhatsApp ad, a report, keywords, audiences): a tool missing from your list is NEVER proof the feature is missing. Then run the tool with call_tool. A tool that is failing or needs a connector this workspace has not made is ranked last and marked, never hidden — pass onlyHealthy:true if you want those left out. Free, read-only.
| Argument | Type | Description |
|---|---|---|
| group | string | limit to one group: core, research, create, channels, channel_admin, analytics, ads, files, workspace |
| limit | number | how many to return (default 12, max 40) |
| query | string | words from the task or the tool name, e.g. "lead form", "whatsapp", "google ads keyword", "meta insights" |
| onlyHealthy | boolean | leave out tools that are failing their recent calls or that need a connector this workspace has not made. Default false — nothing is hidden unless you ask, because a missing row reads as a missing capability. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"group": {
"type": "string",
"description": "limit to one group: core, research, create, channels, channel_admin, analytics, ads, files, workspace"
},
"limit": {
"type": "number",
"description": "how many to return (default 12, max 40)"
},
"query": {
"type": "string",
"description": "words from the task or the tool name, e.g. \"lead form\", \"whatsapp\", \"google ads keyword\", \"meta insights\""
},
"onlyHealthy": {
"type": "boolean",
"description": "leave out tools that are failing their recent calls or that need a connector this workspace has not made. Default false — nothing is hidden unless you ask, because a missing row reads as a missing capability."
}
}
}FINISH a 480p Seedance 2.5 DRAFT in 1080p: the model regenerates THAT take (same scene, shots, motion, sound) at full resolution. Every 480p Seedance 2.5 render (generate_video / render_ad at resolution 480p) is a draft, finishable for 7 days; its result carries `draft` {id, expiresAt, finishCredits}. Pass that id, or the draft video URL. It is the full-resolution route for a draft (upscale_video is for a video that is not a draft); priced like a 1080p render of its length. Paid: call with dryRun first, state the credits, run only when the user agrees. Captions, end card or music added after the draft are not re-applied. Not a draft or expired: a plain refusal; use upscale_video or a fresh 1080p render.
| Argument | Type | Description |
|---|---|---|
| draft | string | the draft id from the render result (draft.id) |
| video | string | or: the URL of the 480p draft video |
| dryRun | boolean | true = return the estimated credits and render nothing |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"draft": {
"type": "string",
"description": "the draft id from the render result (draft.id)"
},
"video": {
"type": "string",
"description": "or: the URL of the 480p draft video"
},
"dryRun": {
"type": "boolean",
"description": "true = return the estimated credits and render nothing"
}
}
}Post-process an EXISTING rendered video (its served mp4 URL) with the proven direct-response 'reviewer' finish and/or a film-grain pass — no AI model, ~30s, billed for its compute only. pills=true composites a header pill (e.g. '10/10 would buy again'), a brand-accent sub-pill, and 3-4 green-check proof pills cascading in on the beat (YOU author the copy: header ≤40 chars, sub ≤34, each point ≤44 — concrete real benefits, never fabricated stats). grain=true applies a subtle camera-grain finish that makes photoreal AI renders look phone-shot ('less AI') — works alone or with pills. Returns a NEW video; the original is untouched.
| Argument | Type | Description |
|---|---|---|
| sub | string | accent sub-pill copy, ≤34 chars (usually the product/brand) |
| grain | boolean | default false — anti-AI film-grain finish |
| pills | boolean | default true — set false for a grain-only pass |
| accent | string | brand accent hex for the sub-pill |
| header | string | header pill copy, ≤40 chars (required when pills is on) |
| points | array | 3-4 proof points, ≤44 chars each |
| videoUrl* | string | the served URL of the video to finish (from a previous render/job); a URL, not a local file path; use upload_file for local files |
Surgically re-render ONE time window (1.5-8s) of an existing rendered video and splice it back on the VIDEO TRACK ONLY — the rest of the video and ALL audio stay byte-identical. Use when one beat/shot is broken ('the shot at 8 seconds glitches') and a full re-render would waste the parts that worked; bills only the replacement clip's seconds (~1/3 of a full render). Do NOT pick a window covering spoken dialogue (a video-only splice under speech breaks lip-sync) — pass speechWindows to enforce this. A FINISHED VIDEO-BLOCKS FILM (make_explainer's explainer or faceless channel; get_job says it is editable) is edited ONE BLOCK at a time instead: pass `job` (its job id) and `block` (1-based) or `atSeconds`, with `prompt` for a different shot and/or `line` for new narration words. Only that block is re-rendered and/or re-read; every other block, take, the music and the captions are the film's own and stay aligned. It is priced as that block alone and charged exactly what it quotes (dryRun). The original film is kept; a new film comes back, and it can be edited again. Feedback maps to one named move, checked by reading the parameters back, never by re-rendering: quieter/louder (music db, audio_gain), faster/slower (speed; slower = zooms and pans at 0.7x), tighten (trim heads, tails, silences), retime (to a word anchor), recut (order, in-out points), regrade (timeline exposure/contrast/saturation keys).
| Argument | Type | Description |
|---|---|---|
| job | string | a finished video-blocks film (explainer / faceless channel): its job id. Edits one block of it instead of splicing a window |
| line | string | with `job`: the new narration for that block, the exact words to say (said as written, never rewritten; refused for free when it cannot fit the block) |
| block | number | with `job`: which block, 1-based (block 3 = 20-30s of a film of 10s blocks) |
| dryRun |
Delete a saved Memory item by its id (from list_memory). Records a cross-device delete so it doesn’t come back. Minor + re-creatable (you can remember it again), so no confirm needed.
| Argument | Type | Description |
|---|---|---|
| id* | string | the memory item id (from list_memory) |
| brand | string | profile id/name from list_brands, this call only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the memory item id (from list_memory)"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
}
}
}Make an exported sheet readable: bold the header row, FREEZE it so it stays visible while scrolling, and auto-size the columns so nothing is cut off. Worth calling right after create_sheet — a raw export with unsized columns and a header that scrolls away is the difference between a spreadsheet someone reads and one they close. Changes no cell VALUE, so it is never gated. The defaults do all three on the first tab; pass `tab` to pick another, freezeRows:0 to skip freezing, boldHeader:false or autoResize:false to skip those.
| Argument | Type | Description |
|---|---|---|
| tab | string | tab title or numeric sheetId (default: the first tab) |
| sheetUrl | string | Google Sheets link, instead of spreadsheetId |
| autoResize | boolean | auto-size columns to fit their content; default true, false skips it |
| boldHeader | boolean | bold the header row; default true, false skips it |
| freezeRows | number | how many top rows to freeze (default 1, 0 = none) |
| spreadsheetId | string | Google spreadsheet id; use this or sheetUrl |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"tab": {
"type": "string",
"description": "tab title or numeric sheetId (default: the first tab)"
},
"sheetUrl": {
"type": "string",
"description": "Google Sheets link, instead of spreadsheetId"
},
"autoResize": {
"type": "boolean",
"description": "auto-size columns to fit their content; default true, false skips it"
},
"boldHeader": {
"type": "boolean",
"description": "bold the header row; default true, false skips it"
},
"freezeRows": {
"type": "number",
"description": "how many top rows to freeze (default 1, 0 = none)"
},
"spreadsheetId": {
"type": "string",
"description": "Google spreadsheet id; use this or sheetUrl"
}
}
}ANIMATE A PHOTO so it talks: the presenter in `image` says `script` word for word, a separate voice lip-synced onto the still. Use it ONLY when the user asks for an animated photo / talking photo / lip-sync look by name; for any other talk-to-camera or spokesperson ask use generate_video with `speak` (the person filmed saying it, which reads as real footage). About 1-3 min, holds the pose steady, 480p/720p. The per-second credit price is in hermoso_capabilities (avatarEngines); dryRun:true returns this exact job's hold without rendering. Blocks until done where the host allows, else returns a job id to poll with get_job. Requires canAvatar. Spends credits; a refusal before rendering costs nothing.
| Argument | Type | Description |
|---|---|---|
| seed | integer | fixed seed, only for an engine other than standard |
| image* | string | local path or URL of the presenter portrait. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded. |
| voice | string | voice name (Aria / Sarah / George / Adam). Leave it out and the voice matches the person in `image`; if nobody can be read, the call is refused free asking for one |
| dryRun | boolean | return the credits this exact job would hold, without rendering |
| engine | string | leave out for the standard engine; only an engine listed in hermoso_capabilities avatarEngines is accepted |
| prompt |
Use this when the user asks for an image ad, ad creative, product photo or social post graphic for their business or product. It renders a finished image on a real image model, on their saved brand (product photos, logo), and returns a URL ready to post. For a video, use render_ad. refImages (paths or URLs) composite a real product into the scene. MULTI-BRAND: hydration uses the SAVED brand; for a different brand (a fresh draft_brand) pass its own productImages/logo as refImages with useBrand:false, or the wrong product lands in the shot. Product photos also pick the ENGINE (the compositing model), so a named `model` is honoured only when no references ride; raw:true or useBrand:false keeps it. model = a catalog id from hermoso_capabilities (omit for the default). A PRODUCT IN A PERSON’S HANDS, or a garment on them: name an id marked `needsRefs` with a `refsMax`; person photo first, then up to three product/garment photos. It EDITS THE PHOTOGRAPH: the person is RE-POSED (stance and hands change; face, clothing, setting and light are kept), so it is the wrong tool when the rest must stay untouched. AN ANIMATED GIF OR AN SVG: format "gif" (a short looping animation, frames + frameMs) or "svg" (an editable vector) on gpt-image-2.6 / gpt-image-2.6-flare, from the prompt alone (no refImages). RAW MODEL ACCESS: raw:true dispatches your prompt to the model BYTE-IDENTICAL — no rewriting, no appended guidance, no negative prompt, no brand references attached on your behalf. Credits, the durable delivery of the finished asset and the per-model validation are unchanged. Seconds. Spends credits.
| Argument | Type | Description |
|---|---|---|
| job | string | what the picture is for. With no `model` named it renders on the engine judged best for that job (hermoso_capabilities `jobEngines.image` lists them): static_ad (the default when neither `model` nor `job` is given) = a finished image ad with its headline, copy and button on the picture; product_photo = a studio, lifestyle, flat-lay or spotlight photo of the real product; marketplace = a listing card; thumbnail = a video cover (make_thumbnail is the full thumbnail pipeline); character_sheet = a multi-view sheet of one person; vector = a logo, icon or flat illustration as an editable SVG (it takes no reference pictures, so the saved brand's photos stay off it). A named `model` always wins |
RAW music: describe it (genre, mood, instruments, tempo), get an instrumental MP3. No length: a full track at a flat fee (explainerMusicCredits in hermoso_capabilities). With seconds: by the second up to about 38 s. A named model always runs.
| Argument | Type | Description |
|---|---|---|
| model | string | minimax-music-3 or lyria-3.5 |
| prompt* | string | the music in words, e.g. 'lo-fi jazz, brushed drums, 80 bpm' |
| seconds | number | track length, up to 300 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"prompt"
],
"properties": {
"model": {
"type": "string",
"description": "minimax-music-3 or lyria-3.5"
},
"prompt": {
"type": "string",
"description": "the music in words, e.g. 'lo-fi jazz, brushed drums, 80 bpm'"
},
"seconds": {
"type": "number",
"description": "track length, up to 300"
}
}
}MAKE a sound effect or ambience that does not exist yet, from a description, as an MP3: a whoosh, a door slam, a cash-register ding, rain on a window, a busy cafe. (find_sound FINDS an existing sound, a meme or trending TikTok sound; this MAKES one.) seconds = length, 0.5-30 (default 5); loop:true for a loopable bed. engine: omit for Seed Audio 1.0 (length and loop are asked for in words, so the length is approximate), or 'eleven-sfx' (ElevenLabs Sound Effects v3: exact length, a real loop flag, description ≤450 characters). Lay it under a video with post_edit (music.track takes its link). dryRun:true returns the exact credits and makes nothing; the price quoted is the charge. Paid, priced by length (hermoso_capabilities -> sound effects).
| Argument | Type | Description |
|---|---|---|
| loop | boolean | true for a loopable bed |
| dryRun | boolean | true = the exact credits only, nothing made or charged |
| engine | string | omit for 'seed-audio' (Seed Audio 1.0), or 'eleven-sfx' (ElevenLabs Sound Effects v3) |
| seconds | number | length in seconds, 0.5-30 (default 5) |
| describe* | string | the sound in words: what makes it, where, how it moves, e.g. 'a fast whoosh past the ear' or 'a busy cafe, cups and low murmur' |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"describe"
],
"properties": {
"loop": {
"type": "boolean",
"description": "true for a loopable bed"
},
"dryRun": {
"type": "boolean",
"description": "true = the exact credits only, nothing made or charged"
},
"engine": {
"type": "string",
"description": "omit for 'seed-audio' (Seed Audio 1.0), or 'eleven-sfx' (ElevenLabs Sound Effects v3)"
},
"seconds": {
"type": "number",
"description": "length in seconds, 0.5-30 (default 5)"
},
"describe": {
"type": "string",
"description": "the sound in words: what makes it, where, how it moves, e.g. 'a fast whoosh past the ear' or 'a busy cafe, cups and low murmur'"
}
}
}Text generation against the writing-model catalog (Claude, Gemini, GPT, Llama, DeepSeek…) — ad copy, hooks, scripts, rewrites, brainstorms. Prompt-only, no ad assembly (for a finished on-brand creative use plan_ad -> render_ad). BY DEFAULT the model answers as a marketing copywriter (a short house system prompt is applied, which is what you want for ad copy); pass raw:true for a plain, unstyled answer from the model itself with NO system prompt at all. model = a writing-model id from hermoso_capabilities (omit for the default Claude orchestrator). Paid (billed at the writing model's token cost, by length).
| Argument | Type | Description |
|---|---|---|
| raw | boolean | RAW MODEL ACCESS: send the prompt with NO Hermoso system prompt — the model answers as itself rather than as an ad copywriter. Use it whenever the ask is not marketing copy (analysis, code, extraction, a plain question). Default false: the copywriter framing is applied. |
| model | string | a writing-model id from hermoso_capabilities (a Claude / Gemini / GPT / Llama / DeepSeek id) — omit for the default |
| prompt* | string | the writing task / question |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"prompt"
],
"properties": {
"raw": {
"type": "boolean",
"description": "RAW MODEL ACCESS: send the prompt with NO Hermoso system prompt — the model answers as itself rather than as an ad copywriter. Use it whenever the ask is not marketing copy (analysis, code, extraction, a plain question). Default false: the copywriter framing is applied."
},
"model": {
"type": "string",
"description": "a writing-model id from hermoso_capabilities (a Claude / Gemini / GPT / Llama / DeepSeek id) — omit for the default"
},
"prompt": {
"type": "string",
"description": "the writing task / question"
}
}
}Use this when the user asks for a video clip, b-roll or a shot from a description, or names a video model. For a finished video ad, Reel, TikTok ad or UGC video for their business, use render_ad. It renders one continuous clip on a real video model and returns its mp4 URL. A clip runs up to the model’s longest listed duration (the longest takes a multi-beat 30s spot in ONE take with native audio), so never assume an 8–10s cap or stitch what fits one clip; durationSeconds must be one of the model’s durations in hermoso_capabilities. NAME A MODEL in `model` to get it: an unnamed render uses a narrower auto-pool. 1–3 min. refImage anchors the first frame; ttsScript adds a voiceover. AUDIO IS NOT FREE BY DEFAULT: a clip with no audio of its own gets a music bed composed and CHARGED on top (see musicMood and audio), which can cost as much as a cheap draft clip. refVideo (a clip URL) EDITS that clip instead, keeping its canvas and length. RAW MODEL ACCESS: by default small guards are added (label safety with no reference image, a negative prompt where the model takes one, reference lines) and hex colours become names; raw:true dispatches your prompt to the model BYTE-IDENTICAL — no rewriting, no appended guidance, no negative prompt, no brand references attached on your behalf. Credits, the durable delivery of the finished asset and the per-model validation are unchanged. Spends credits (blocked on the Starter plan).
| Argument | Type | Description |
|---|---|---|
| raw | boolean | Send the prompt to the model exactly as written, without the wording Hermoso usually adds (packaging and label guidance, reference lines, colour names for hex codes, quality hints). Use it when you want the model's own interpretation rather than Hermoso's render craft. Two vendor-required fixes still apply: extra @ImageN tokens are dropped and an over-long prompt is trimmed at a sentence (on Veo it is refused with the length and the cap instead). Billing, durable delivery and per-model validation are unchanged. |
| loop | boolean | true = a seamless loop whose last frame flows back into its first. Only models with loop true in hermoso_capabilities; needs refImage and cannot be combined with endImage. |
RAW speech as an MP3: one voice reading a script (text), or a DIALOGUE of two or more people (lines), each in their own voice. Not for adding audio to a video (render_ad and generate_video voice their own spots; change_voice re-voices a finished clip). ONE VOICE: text + engine (default 'seed-audio'; also 'eleven-v4', 'minimax-speech', 'kokoro'…) + voice, a preset from that engine (hermoso_capabilities -> voice engines); a name the engine does not have is REFUSED for free with its real list, and a few engines make their own voice (the reply says which voice spoke). DIALOGUE: lines [{speaker, voice, text}] in speaking order. Each speaker keeps ONE voice, no two speakers share one, a speaker with no voice gets one nobody has; up to 10 voices and 10,000 characters, refused for free past that. Default engine: the dialogue model (Eleven v3 Dialogue, voices Aria, George, Sarah, Roger, Laura, Charlie, Callum, River, Liam, Charlotte, Alice, Matilda, Will, Jessica, Eric, Chris, Brian, Daniel, Lily, Bill, or an ElevenLabs voice id; '[laughs]'-style cues work); or name any voice engine (e.g. 'eleven-v4') to read each line on it and join them in order. dryRun:true returns the exact credits and makes nothing; the price quoted is the charge. Paid, priced by characters (hermoso_capabilities lists credits per 1,000).
| Argument | Type | Description |
|---|---|---|
| text | string | ONE VOICE: the script to speak (≤900 characters) |
| lines | array | DIALOGUE: the turns in speaking order |
| voice | string | ONE VOICE: a preset from the chosen engine (e.g. 'Aria'/'George' on eleven-v4, 'stokie_en' on seed-audio); omit for the engine default |
| dryRun | boolean | true = the exact credits only, nothing made or charged |
What Hermoso ALREADY KNOWS for the active profile: the same saved profile (products, logos, palette, positioning) + learned memory the web Studio uses. Call it when you need to know whether a brand is on file: if hasBrand is true you can omit brand everywhere; if false, onboard with draft_brand. Not a required first step before a render: the create tools read the saved brand by themselves. 0 credits.
| Argument | Type | Description |
|---|---|---|
| brand | string | profile id/name from list_brands, this call only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
}
}
}Fetch one Drive file’s metadata — name, type, size, modified time, a webViewLink to open it and a webContentLink to download it. Pass fileId (from list_drive_files). Read-only.
| Argument | Type | Description |
|---|---|---|
| fileId* | string | the Drive file id (from list_drive_files) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"fileId": {
"type": "string",
"description": "the Drive file id (from list_drive_files)"
}
}
}Poll a render job by id. Returns status (queued|running|done|error), progress, and on done the served media URL. Renders take 1–3 minutes: keep calling this until done/error without asking the user — several calls is normal, not a stall. An id that does not exist on this account answers status "not_found" — that is FINAL: stop polling it, and do not re-fire the render (that double-charges).
| Argument | Type | Description |
|---|---|---|
| id* | string | the job id: starts with job_, from a render tool's jobId or from list_jobs (generate_voice and generate_sfx return their file directly and have no job) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the job id: starts with job_, from a render tool's jobId or from list_jobs (generate_voice and generate_sfx return their file directly and have no job)"
}
}
}Fetch one OneDrive item’s metadata — name, type, size, modified time, a webViewLink to open it and a webContentLink to download it. Pass fileId (from list_onedrive_files). Read-only.
| Argument | Type | Description |
|---|---|---|
| fileId* | string | the OneDrive item id (from list_onedrive_files) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"fileId": {
"type": "string",
"description": "the OneDrive item id (from list_onedrive_files)"
}
}
}Read this account's app settings — the LANGUAGE Hermoso writes ads and copy in (chat replies follow the language the user writes in), the app appearance (theme), and whether the weekly competitor-watch email is on. Same settings as the web app's Settings pane. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Load a bundled skill’s full SKILL.md workflow instructions by name (from list_skills), or a built-in in-app skill by its id (e.g. youtube_script, retention_skeleton). Follow the loaded instructions to run that workflow with the other tools. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| name* | string | bundle name from list_skills, e.g. hermoso-generate |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "bundle name from list_skills, e.g. hermoso-generate"
}
}
}Turn ONE finished static ad into several copies that differ ONLY in the headline, for an A/B test: same picture, product, layout, colours and every other line. Pass `image`, and either `headlines` (your own, up to 10) or `count` (default 5, max 10) to have distinct angles written for you in the saved brand's voice (never inventing numbers, prices, ratings or claims the ad or brand does not state); `brief` steers what to test. The ad's text is read first (3 credits), then one image edit per headline; each output is proofread and flagged (textCheck) if the rendered words do not match, never silently re-rendered. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. The whole batch is priced before anything runs. Returns each headline, its angle and its image URL.
| Argument | Type | Description |
|---|---|---|
| brief | string | what to test, e.g. "price-led vs outcome-led" or "speak to busy parents" |
| count | integer | how many headlines to write when `headlines` is omitted (default 5) |
| image* | string | the finished static ad: URL, Library item URL, upload_file URL or local path |
| fixLabel | boolean | false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product) |
| headlines | array | your own headlines to test (up to 10); omit to have them written |
Probe what this Hermoso account can do RIGHT NOW: available image/video model ids + their credit costs (estimates: a render is charged its actual cost), aspect ratios, video durations, the recipe ids, and the canEdit/canAvatar flags. Call it when you need a specific model id, a credit cost, or a capability you are not sure of. It is NOT a prerequisite for rendering: generate_image, generate_video and render_ad all run with `model` omitted and route to the server’s own default. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Return the credit balance, recent priced calls with their credits, and costModel (the one-sentence rule of what costs credits). THE RULE: only AI model runs and Ad Spy research spend credits; publishing, scheduling, ads management, analytics, comments, DMs and connectors cost no credits on any plan (X is the single per-call exception; xCost gives its exact credits per post, with and without a link). Check before kicking off paid generation; answer "does posting cost credits?" with NO. HOW MANY posts is set by the plan, not credits: the Free plan includes 10 posts a month on up to 3 channels, and a post counts once however many channels it goes to (`posting` says how many are used); every paid plan posts without a limit and runs autopilot (Basic $9.99/mo: unlimited posting and Autopilot, no monthly credits; it includes the same 250+ free credits as the Free plan, monthly credits start on Starter, and Autopilot spends credits). Starter, Pro and Studio: media models at cost + 10% (memberRate); Basic and Free pay standard prices.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}HOOK MULTIPLIER: give ONE finished video ad N NEW OPENING HOOKS, N complete versions to A/B test. A planned hook replaces the first ~1.5-4 s (ends at the source's first cut there, else 3 s; hookSeconds overrides) with a new silent shot on a DIFFERENT named mechanic (list_hooks); the rest of the footage and the WHOLE original soundtrack stay, so every version keeps the source's length, and nobody in the hook talks or shows text. hooks[] adds your own openings: {url} a FOUND viral hook (post link or file) joined in front with the approved bridge (cut before its payoff, its own payoff sound carried across; post_edit price), {prompt} an opening described in words, {mechanic} a named one; with hooks and no count only those are made. To make the found hook's subject your product or creator first: recast_hook. Pass the video's FILE URL (a render, job result, list_library or upload_file). 1-5 versions, default 3. Refused free before billing: a source over 120 s, too short for a 1.5 s hook plus 2 s after it, unreadable, or a social post as the SOURCE (clone_video remakes someone else's ad). COST: quoteOnly:true returns the exact credits free: state that first. A paid planning read (planCredits) runs next, then each planned version bills like fix_beat for the hook's seconds. dryRun:true pays for that read; pass its `plan` back to render exactly those. Returns ONE JOB PER VERSION; call get_job on each until done, never describe a version before its URL arrives. Hooks that show the product use the brand's product photo (productImage overrides; useBrand:false sends none).
| Argument | Type | Description |
|---|---|---|
| plan | any | the `plan` object a previous dryRun returned, to render exactly those hooks without planning again |
| count | number | how many hook versions, 1-5 (default 3) |
| hooks | array | your own openings, one version each |
| notes |
Pull the files in a Google Drive or OneDrive FOLDER into this profile's Library, so they can be used like anything rendered here — published, scheduled, cloned, used as a product photo or a reference. Hermoso downloads each file with the user's own connected account (a Drive/OneDrive file is not public) and stores a durable Hermoso url for each. Give `folderId` from list_drive_files / list_onedrive_files with onlyFolders — omit it for the root. GOOGLE DRIVE ONLY SHOWS WHAT THE USER HANDED OVER: our Drive scope is `drive.file`, so Hermoso can see the files and folders it created plus the ones the user picked with the Google picker in the app, and NEVER their whole Drive. Picking a FOLDER hands over the folder alone, not the files in it, so if a folder comes back empty the reply carries a link that opens the picker inside it, where the user selects every file at once (Cmd+A / Ctrl+A); give them that link. SUBFOLDERS ARE NOT WALKED and Google-native docs (Docs/Sheets/Slides) have no file to download: both are reported back BY NAME rather than silently dropped, along with anything too large or unreadable, so you can tell the user exactly what did and did not come across.
| Argument | Type | Description |
|---|---|---|
| limit | number | how many files to bring across in this call (default 10, max 25). Anything over the limit is listed as skipped so you know what is left. |
| folderId | string | the folder to import, from list_drive_files / list_onedrive_files (onlyFolders:true). Omit for the root of the drive. |
| provider* | string | which cloud — `drive` is Google Drive, `onedrive` is Microsoft OneDrive |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider"
],
"properties": {
"limit": {
"type": "number",
"description": "how many files to bring across in this call (default 10, max 25). Anything over the limit is listed as skipped so you know what is left."
},
"folderId": {
"type": "string",
"description": "the folder to import, from list_drive_files / list_onedrive_files (onlyFolders:true). Omit for the root of the drive."
},
"provider": {
"enum": [
"drive",
"onedrive"
],
"type": "string",
"description": "which cloud — `drive` is Google Drive, `onedrive` is Microsoft OneDrive"
}
}
}INSERT a newly generated scene into a clip you already have: the video plays up to startTime, the new scene (5 to 13 seconds) plays, then the original picks up again at resumeTime. Use for a product close-up, a reaction, a reveal or a missing beat in the middle of an ad without re-rendering the rest. resumeTime = startTime keeps every second of the original; a later resumeTime also cuts the footage in between. Describe the scene in prompt and/or attach up to 4 reference pictures (refImages) or 1 reference clip (refVideos). Clips up to 60 seconds; both times between 1.625 and 60 seconds. Paid render (billed per second of NEW scene); returns the whole video's URL.
| Argument | Type | Description |
|---|---|---|
| video* | string | the source video URL (a render, job result or list_library item) |
| prompt | string | what happens in the new scene: the action, the subject, the camera |
| seconds | number | length of the new scene, 5-13 seconds (default 5) |
| refImages | array | up to 4 picture URLs of what should appear (the product, a person, a place) |
| refVideos | array | up to 1 clip URL whose look or motion the scene follows |
| startTime* | number | seconds into the clip where the new scene starts (1.625-60) |
| resolution | string |
Invite someone to this profile by email (role: member = read-only on billing, admin = full). This SENDS a real email invite / share link — an account change. Confirm the exact email + role with the user, then call with confirm:true.
| Argument | Type | Description |
|---|---|---|
| role | string | default member |
| email* | string | the invitee’s email |
| confirm | boolean | REQUIRED true — this invites a real person |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"email"
],
"properties": {
"role": {
"enum": [
"member",
"admin"
],
"type": "string",
"description": "default member"
},
"email": {
"type": "string",
"description": "the invitee’s email"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — this invites a real person"
}
}
}On a connector that several teammates can contribute their OWN account to (see list_connectors — the row reports multiContributor), remove YOURS from this profile: your stored credential is dropped and the accounts you shared stop being shared. Your teammates' accounts on the same connection keep working, and nothing changes at the provider — reconnecting in a browser shares again. Use this instead of disconnect_connector when the connection is not yours to remove: disconnect_connector revokes the grant at the provider and only the person who created the connection may call it.
| Argument | Type | Description |
|---|---|---|
| provider* | string | provider id exactly as list_connectors reports it, e.g. "linkedin", "tiktok_ads", "meta" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider"
],
"properties": {
"provider": {
"type": "string",
"description": "provider id exactly as list_connectors reports it, e.g. \"linkedin\", \"tiktok_ads\", \"meta\""
}
}
}List every profile on this account (id + name; a profile is a brand, a creator or a personal workspace) and which one this connection acts on, PLUS any profile another account shared with you. Call this, then use_brand to switch; a SHARED profile switches the same way (pass its name or the profile id printed here). If a profile looks empty when the app shows it full, you are acting on a different one: call this first. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Show every identity a connected account can act as, and which ones this PROFILE is currently allowed to use — Facebook Pages + Instagram + Meta ad accounts, Google Ads customers, LinkedIn company Pages (and the personal profile), Pinterest ad accounts, Microsoft Advertising accounts. One person often administers several; only the ticked ones can be posted to or spent from. Call this before set_connector_accounts, and let the USER pick — never guess. Providers: tiktok, x, youtube, threads, bluesky, telegram, pinterest, instagram, meta, google_ads, linkedin, pinterest_ads, linkedin_ads, reddit_ads, apple_ads, microsoft_ads, google_business, google_analytics, snapchat_ads, x_ads, tiktok_ads, google_tag_manager, google_search_console, bing_webmaster. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| provider* | string | which connector’s accounts to list |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider"
],
"properties": {
"provider": {
"enum": [
"tiktok",
"x",
"youtube",
"threads",
"bluesky",
"telegram",
"pinterest",
"instagram",
"meta",
"google_ads",
"linkedin",
"pinterest_ads",
"linkedin_ads",
"reddit_ads",
"apple_ads",
"microsoft_ads",
"google_business",
"google_analytics",
"snapchat_ads",
"x_ads",
"tiktok_ads",
"google_tag_manager",
"google_search_console",
"bing_webmaster"
],
"type": "string",
"description": "which connector’s accounts to list"
}
}
}List the third-party accounts connected to this workspace (Meta, Google Ads, Google Drive/Sheets/Docs, YouTube, LinkedIn, OneDrive, Slack, …) — provider, status and the connected account label — PLUS which providers are available to connect. IT ALSO FLAGS A CONNECTION WHOSE PERMISSIONS ARE OUT OF DATE: a provider writes its granted permission set into the token at consent time, so a connection authorized before a permission was approved does not carry it and never will — those calls are refused by the provider and no retry or wait can change it. Check this FIRST when a connected provider starts refusing things. The fix is a browser: the user reconnects under Workspace > Connectors. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}List this workspace’s SAVED CREATORS — the reusable on-camera cast (AI creators made here, a person pulled from a social profile, a consented photo upload). Read-only, FREE. Each entry gives the name, the PORTRAIT URL, where the portrait came from and whether a real person’s likeness consent is on file, how many extra pose plates exist, and any chosen or cloned voice. TO PUT ONE IN A FINISHED AD, pass their id or name as render_ad’s `creator` — that casts them for the whole spot (and skips the character-portrait render, so it costs less than not casting anyone). THE PORTRAIT URL IS THE REUSE HANDLE for the raw lanes — pass it as generate_avatar’s `image` (a talking clip of them), generate_video’s `refImage` (they star in the scene), recast_motion’s `image` (they perform a reference clip’s motion), or generate_image’s `refImages`. CALL THIS BEFORE OFFERING TO GENERATE A NEW PERSON: re-casting somebody the workspace already has keeps the SAME face across every ad, while a fresh person costs credits and breaks that continuity. An empty answer means the workspace genuinely has no cast yet — say so and offer generate_avatar / save_creator, never invent a roster.
| Argument | Type | Description |
|---|---|---|
| limit | number | max creators to return (default 24) |
| gender | string | filter the PRESET creators by gender (the saved cast is never filtered) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max creators to return (default 24)"
},
"gender": {
"enum": [
"female",
"male"
],
"type": "string",
"description": "filter the PRESET creators by gender (the saved cast is never filtered)"
}
}
}List the Google Drive files & folders Hermoso can reach: the ones it created plus any the user picked with the Google file picker in the app (drive.file, never their whole Drive). This is how you find a picked file's id. Filter by query (name contains …), folderId, or onlyFolders:true; paginate with pageToken. Read-only. A FOLDER: picking a folder hands over the folder alone, not its files, so a folder can list empty or partly; the reply then carries `pickUrl`, an app link that opens the picker inside that folder (Cmd+A / Ctrl+A selects every file and subfolder). Give the user that exact link, then list again. recursive:true walks every subfolder Hermoso can see (each row has its `path`) and reports picked files whose folder it cannot see.
| Argument | Type | Description |
|---|---|---|
| query | string | only files whose name contains this |
| folderId | string | list the contents of this folder id |
| pageSize | number | rows per page (1–200, default 50) |
| pageToken | string | cursor from a previous call |
| recursive | boolean | with folderId: the whole tree |
| onlyFolders | boolean | list folders only |
| includeTrashed | boolean |
The errors actually recorded against this workspace, GROUPED by fingerprint — the same failure at the same call site is one row with a hit count and first/last seen, sorted defects-first. Each row says whose side it is: `ours` (a defect worth fixing), `user` (a refusal we deliberately authored, e.g. not-connected or out-of-credits), or `unknown` (a vendor 4xx we cannot attribute — never guessed). Free text, tokens, emails and creative are redacted before anything is stored, so an input echo shows shapes and lengths, not content. Filter by surface (http/mcp/agent/job/client) or kind. Read-only, 0 credits. Scoped to your own workspace; an operator whose client carries the admin key sees every account.
| Argument | Type | Description |
|---|---|---|
| kind | string | 'ours' = a defect; 'user' = a refusal we authored; 'unknown' = we could not tell |
| limit | number | how many groups to return (default 50, max 200) |
| since | string | ISO timestamp — only groups last seen at or after this |
| surface | string | where it happened: http (an API route), mcp (an agent tool), agent (the in-app Studio agent), job (an async render/publish), client (a browser crash) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"kind": {
"enum": [
"ours",
"user",
"unknown"
],
"type": "string",
"description": "'ours' = a defect; 'user' = a refusal we authored; 'unknown' = we could not tell"
},
"limit": {
"type": "number",
"description": "how many groups to return (default 50, max 200)"
},
"since": {
"type": "string",
"description": "ISO timestamp — only groups last seen at or after this"
},
"surface": {
"enum": [
"http",
"mcp",
"agent",
"job",
"client"
],
"type": "string",
"description": "where it happened: http (an API route), mcp (an agent tool), agent (the in-app Studio agent), job (an async render/publish), client (a browser crash)"
}
}
}The curated menu of VISUAL scroll-stop HOOKS (how an ad opens) and SETTINGS (where it is staged) that plan_ad and render_ad accept, PLUS this brand's measured traction per hook. Call it before planning an ad to pick a hook deliberately instead of letting the model improvise one, and call it after publishing to see which ones are actually landing. Three things it will not do: it never recommends a hook from thin data — a verdict is SUPPRESSED below 5 measured posts and the reason is stated; it never compares across channels; and it reports hooks you have NEVER TRIED as a fact, not as advice, because 'you haven't tried this' is an observation and 'you should' would be a verdict drawn from zero data. A hook marked unusable in this brief says WHY (an on-screen-text hook cannot ride an authentic/UGC render, which carries zero on-screen text). Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| tier | string | product tier, used with category — changes the FINISH of the room, never the room. Default premium. |
| channel | string | restrict the performance half to one channel (facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky) |
| category | string | the product category (e.g. 'skincare serum', 'protein powder', 'sunglasses') — returns the setting our Location x Tier matrix puts that category in, with the reason |
| authentic | boolean | true if the planned ad is an authentic/UGC/creator-register render — on-screen-text hooks are then reported unusable, with the reason |
List the most recent render jobs + how many are currently running, so you can report on or resume in-flight work.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Browse this workspace's Library — every image/video generated in the Studio, and uploads (source:'upload'), newest first (the same Library the web app shows). Returns served URLs you can open directly or hand to fetch_asset for a download link, plus each asset's id (L0, L1…, which plan_ad and render_ad `inserts` take), kind, model, and age. Free, read-only.
| Argument | Type | Description |
|---|---|---|
| kind | string | filter by asset kind (default 'all') |
| limit | number | max assets to return (default 20, max 60) |
| uploads | boolean |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"kind": {
"enum": [
"image",
"video",
"document",
"all"
],
"type": "string",
"description": "filter by asset kind (default 'all')"
},
"limit": {
"type": "number",
"description": "max assets to return (default 20, max 60)"
},
"uploads": {
"type": "boolean"
}
}
}List the durable facts & preferences saved in this workspace’s Memory (what the studio remembers about the brand, audience, taste, and do/don’t rules) — the same Memory the web app shows. These shape every future ad. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | max items (default 50, max 200) |
| category | string | filter to one bucket (Brand/Audience/Taste/Do/Don’t/Preference) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max items (default 50, max 200)"
},
"category": {
"type": "string",
"description": "filter to one bucket (Brand/Audience/Taste/Do/Don’t/Preference)"
}
}
}List the connected Facebook Page's or Instagram account's OWN existing posts — id, caption, permalink, publish date and format. Use it for "how did our last few posts do", to find a post the user describes loosely, or before backfill_posts. Pass target:'instagram' for the linked IG account (Stories are excluded — Meta's media edge does not return them); Facebook hides unpublished drafts unless you ask for them. Only ever reads a Page the brand has connected. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| brand | string | WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done. |
| limit | number | how many posts (default 25, max 100) |
| cursor | string | paging cursor returned by a previous call |
| pageId | string | which connected Page — omit when the profile has only one |
| tagged | boolean | INSTAGRAM — list posts OTHER accounts tagged the brand in (UGC, creator posts, partner mentions), with each author’s @handle, instead of the brand’s own posts. Free. |
| target | string | default facebook; 'instagram' reads the Page's linked IG business account |
Find the Notion pages and databases the user shared with Hermoso, by title, newest edited first. Gives the page id that save_to_notion, append_to_notion_page and read_notion_page take, or the database id to save rows into. Read-only, free. Needs Notion connected (Settings > Connectors > Notion).
| Argument | Type | Description |
|---|---|---|
| type | string | limit results to pages or to databases; omit for both |
| limit | number | 1-100, default 25 |
| query | string | words in the title |
| cursor | string | from the previous page |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"type": {
"enum": [
"page",
"database"
],
"type": "string",
"description": "limit results to pages or to databases; omit for both"
},
"limit": {
"type": "number",
"description": "1-100, default 25"
},
"query": {
"type": "string",
"description": "words in the title"
},
"cursor": {
"type": "string",
"description": "from the previous page"
}
}
}List files & folders in the user’s OneDrive — the root by default, a folder’s contents (folderId), or a name search (query). onlyFolders:true lists folders only. Paginate with pageToken (the cursor from a previous call). Read-only.
| Argument | Type | Description |
|---|---|---|
| query | string | search — only items whose name matches this |
| folderId | string | list the contents of this folder id |
| pageSize | number | rows per page (1–200, default 50) |
| pageToken | string | cursor from a previous call |
| onlyFolders | boolean | list folders only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"query": {
"type": "string",
"description": "search — only items whose name matches this"
},
"folderId": {
"type": "string",
"description": "list the contents of this folder id"
},
"pageSize": {
"type": "number",
"description": "rows per page (1–200, default 50)"
},
"pageToken": {
"type": "string",
"description": "cursor from a previous call"
},
"onlyFolders": {
"type": "boolean",
"description": "list folders only"
}
}
}List the PLAYBOOKS saved in this workspace — the reusable strategy cards (winning hooks, angles, formats and the concrete plays to run) kept from teardowns, angle mining and creatives worth repeating. The same Playbooks the web app's Playbooks tab lists. Read one before planning an ad so you re-run what already worked instead of starting cold. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| full | boolean | true to return every hook/angle/play in the text, not just the headline counts |
| limit | number | max playbooks to return (default 25, max 100) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"full": {
"type": "boolean",
"description": "true to return every hook/angle/play in the text, not just the headline counts"
},
"limit": {
"type": "number",
"description": "max playbooks to return (default 25, max 100)"
}
}
}List the product photos ALREADY saved in your workspace — the brand's product library plus any app-store screens (also surfaces photos locked in your OTHER creations, since a set product lands in the shared library). FREE — returns each photo's url + the product it shows (the shop's own title when one is on file). These are already saved: never ask the user to upload them again. Call it before set_product_image to see the existing photos you can reuse. Reads YOUR saved brand (pass brandId to target a specific profile — that switches this key's active profile like use_brand).
| Argument | Type | Description |
|---|---|---|
| brandId | string | a profile id/name from list_brands whose product library to list; omit to use the active profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands whose product library to list; omit to use the active profile"
}
}
}The brand's product catalog: each product's id, title, page link, price (and whether it is current), photos, its own selling points and audience, and when it was last checked against its live page. Free. `refresh:'stale'` re-reads rows older than a day first, `refresh:'all'` re-reads every page. Use the ids with make_static_ads.
| Argument | Type | Description |
|---|---|---|
| brandId | string | a profile id/name from list_brands; omit for the active profile |
| refresh | string | re-read product pages before listing: 'stale' (older than a day) or 'all' |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands; omit for the active profile"
},
"refresh": {
"enum": [
"stale",
"all"
],
"type": "string",
"description": "re-read product pages before listing: 'stale' (older than a day) or 'all'"
}
}
}List every post Hermoso has recorded publishing for this brand, newest first, across all channels — channel, permalink, caption, format, the HOOK and SUBJECT it was written to, and its measured engagement. The WHOLE history, no cap: pass the reply's nextCursor as `cursor` for older posts. Each row says how it was recorded: 'captured' (written at publish time — the hook is what the author intended) or 'backfilled' (reconstructed from the platform afterwards). A dash for engagement means the platform reported no number — NOT zero. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| brand | string | WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done. |
| limit | number | max posts (default 50, max 200), newest first |
| cursor | string | nextCursor from a previous reply: the next, older page |
| channel | string | filter to one channel: facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky, google_business |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brand": {
"type": "string",
"description": "WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done."
},
"limit": {
"type": "number",
"description": "max posts (default 50, max 200), newest first"
},
"cursor": {
"type": "string",
"description": "nextCursor from a previous reply: the next, older page"
},
"channel": {
"type": "string",
"description": "filter to one channel: facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky, google_business"
}
}
}The tabs in a Google Spreadsheet, each with its name, numeric sheetId, row/column count and position. Call this BEFORE naming a tab in update_sheet / clear_sheet_range / manage_sheet_tabs / format_sheet, and before proposing to delete one — it is how you learn what the file actually contains instead of guessing at a name. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| sheetUrl | string | a Google Sheets URL — the id is extracted from it |
| spreadsheetId | string | the spreadsheet id (from create_sheet, or list_drive_files for one the user picked) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"sheetUrl": {
"type": "string",
"description": "a Google Sheets URL — the id is extracted from it"
},
"spreadsheetId": {
"type": "string",
"description": "the spreadsheet id (from create_sheet, or list_drive_files for one the user picked)"
}
}
}List the bundled Hermoso SKILLS — multi-step workflow instructions (SKILL.md) that orchestrate the other tools (research an ad space, plan+render a finished ad, product photoshoot, raw generation) — plus the in-app strategy skills and creative recipes. Call get_skill to load a bundle. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}List this workspace's SWIPEFILE — the saved-ad research board: every named collection and the ads/creatives kept in it (advertiser, headline, body copy, media URL, platform, when it was saved, and any taste tags). The SAME board the web app's Swipefile tab shows. Use it to answer "what have we saved?", to mine the user's own taste before planning an ad, or to find a reference to remix. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | max ads to return (default 50, max 500) |
| collection | string | only list ads in this collection (by name or id) — omit for every collection |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max ads to return (default 50, max 500)"
},
"collection": {
"type": "string",
"description": "only list ads in this collection (by name or id) — omit for every collection"
}
}
}List the members of the current profile — email, role (admin/member) and status. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Ready-to-run ad BRIEFS by industry: dropshipping, ecommerce, mobile apps, restaurants, real estate and local services (12 trades: plumbers, electricians, hvac, roofers, landscapers, house-cleaners, pest-control, auto-repair, salons, med-spas, dentists, gyms). Compact list by default; pass `id` for one template in full: its brief (filled with the active profile's name, and the trade's specifics when `trade` is given), what the user must supply, the recommended format/aspect/length, and its web deep link. To run one, fill the [square-bracket] slots from what the user gave you, then pass the brief to plan_ad as `product` (with its format and durationSeconds) and render the plan (render_ad for video, generate_image for images). Every part is editable: a template is a shortcut, not a lane, so change or ignore anything that does not fit. These are NOT the native HTML formats of make_template_ad. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| id | string | one template id (from the list) to return in full, with its brief |
| trade | string | for local services: plumbers, electricians, hvac, roofers, landscapers, house-cleaners, pest-control, auto-repair, salons, med-spas, dentists or gyms. Fills the trade specifics into the brief |
| vertical | string | dropshipping | ecommerce | mobile-app | restaurants | real-estate | local-services; omit for all |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "one template id (from the list) to return in full, with its brief"
},
"trade": {
"type": "string",
"description": "for local services: plumbers, electricians, hvac, roofers, landscapers, house-cleaners, pest-control, auto-repair, salons, med-spas, dentists or gyms. Fills the trade specifics into the brief"
},
"vertical": {
"type": "string",
"description": "dropshipping | ecommerce | mobile-app | restaurants | real-estate | local-services; omit for all"
}
}
}Read what a brand's MARKET RADAR (set_competitor_watch) has found: each watched brand's new ads, the winners (ads past 30 days running, with a ready brief to make your own version), the trending formats and top posts for its topics, and the ideas to make, plus its state (lastRun, nextRunAt, lastRunOk, paused) and the quote for its next check. Findings marked `seed:true` are what a brand was already running at its first check, not new launches. Read-only and free; it never runs a check.
| Argument | Type | Description |
|---|---|---|
| brand | string | profile id/name from list_brands, this call only |
| limit | number | max ads (default 25, max 75) |
| competitor | string | only this watched brand's ads (exact name as returned) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"limit": {
"type": "number",
"description": "max ads (default 25, max 75)"
},
"competitor": {
"type": "string",
"description": "only this watched brand's ads (exact name as returned)"
}
}
}Translate the on-image text of ONE finished static ad into other languages and keep everything else: same picture, layout, typeface, colours, logo and product. Pass `image` and `languages` (up to 5, e.g. ["Spanish", "German", "French (Canada)"]). The ad's text is read (3 credits), translated the way a native copywriter in each market would write it (brand and product names, URLs and prices kept as written), then one image edit per language; each output is proofread and flagged (textCheck) if the words do not match, never silently re-rendered. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. Priced before it runs. For a VIDEO use dub_video.
| Argument | Type | Description |
|---|---|---|
| image* | string | the finished static ad: URL, Library item URL, upload_file URL or local path |
| fixLabel | boolean | false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product) |
| languages* | array | target languages, by name |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"image",
"languages"
],
"properties": {
"image": {
"type": "string",
"description": "the finished static ad: URL, Library item URL, upload_file URL or local path"
},
"fixLabel": {
"type": "boolean",
"description": "false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 5,
"minItems": 1,
"description": "target languages, by name"
}
}
}ONE-CLICK EFFECTS, the same ones as the /tools pages: a photo goes in, a styled picture comes out, and most can then become a 5-second clip. Plush toy, 3D figure, mascot, comic book, pixel game, billboard ad, giant product, bullet time, packshot, outfit swap, headshot, relight and many more. Name no `effect` for the free list. `effect` alone: what it takes. `effect` + `images`: a free quote; add confirm:true to make the picture. Then `effect` + `still` (that picture's URL): a free quote for the clip; confirm:true makes it. `describe` adds anything of your own. For a look not on the list use generate_image. A real person's photo follows the same likeness rules as every render.
| Argument | Type | Description |
|---|---|---|
| text | string | a line printed exactly, on an effect that takes one |
| still | string | CLIP STEP: the finished picture to animate |
| effect | string | effect id from the list |
| images | array | its pictures, in the order listed: URL, local path or Library id |
| speech | string | CLIP STEP: a line said in the clip, on an effect that speaks |
| confirm | boolean | true = run it (real spend); without it, a free quote |
| options | object | its listed options, e.g. {material:"knit"}; "custom" means use `describe` |
AI HOST EPISODE: format 'host_episode' = one presenter talking to camera, the camera changing every piece (frontal, three-quarter, close), the same host and set throughout, native voice, 16:9. Host = `creator` (saved or preset) or `hostImage`; words = `topic` (written for you) or `script` (verbatim). It returns a 480p DRAFT; HD (720p) is a SEPARATE call with `fromDraft` (quote it with dryRun, run it only when the user asks). Otherwise: turn a TOPIC into a finished narrated explainer video, in one of TWO LANES (`lane`). 'blocks' (the default) = 10-second VIDEO blocks, one narrated line per block, hard cuts, a music bed under the voice: an EXPLAINER renders on Gemini Omni at 720p (9:16 by default, or 16:9), a FACELESS CHANNEL video (`format:'faceless_channel'`, or channel history / kids / fairytale) on MiniMax H3 at 2K (16:9 by default) with five cuts per block. 'stills' = a picture film: writes a sectioned script, paints a BURST of pictures per section (about one every 1.5s — most of them one-detail edits of the frame before, so it reads as movement rather than a slideshow), narrates each section with TTS, holds each picture PERFECTLY STILL for its own slice of the narration (the motion is the CUT RATE — a slow move on a still shimmers), then composites the end card (and any on-screen text you asked for) with the Chrome+ffmpeg engine the ads use (text is never model-painted, so it never garbles). BURNED ON-SCREEN TEXT IS OFF BY DEFAULT — the narration carries the point and the pictures carry the story, so the film ships clean unless the user asks otherwise; `captions:true` adds held key points and `subtitles:true` adds subtitles timed to the narration (see both). The stills lane is an image film WITH motion, not N video-model renders — that's what keeps it affordable. `style` picks the visual family: the default 'cinematic' is photoreal editorial; every other id is a STYLED, strictly non-photoreal look (illustrated / collage / clay / pixel …) that first renders ONE style-key image and then locks every scene to it, so the whole film holds one look. Cost at the default frame density: a ~130-credit hold for a 60s explainer on the default style, ~100 styled; `frameDensity:'lean'` roughly halves it and `'minimal'` (one picture per section) is ~30. All settle to the exact per-frame image + narration spend (a longer target = more sections = more). Takes SEVERAL minutes — one image render per frame; independent frames are painted concurrently, so it is far faster than the frame count suggests. Needs the writing model and a narration voice engine connected. NOT the tool for a short product ad — use render_ad or generate_video for those, and make_template_ad for the deterministic native formats.
A reaction picture (image, or video from videoStart) with its sound, cut to when the sound lands (or seconds): a 1080x1920 clip for post_edit join.
| Argument | Type | Description |
|---|---|---|
| image | string | still picture URL; use this or video |
| sound* | string | sound URL (find_sound returns one), or a video whose own audio is used |
| video | string | video URL to take the picture from, starting at videoStart |
| seconds | number | fixed insert length, 0.2 to 6 seconds, instead of when the sound lands |
| soundEnd | number | seconds into the sound where it ends |
| soundStart | number | seconds into the sound where the insert's sound starts |
| videoStart | number | seconds into the video where the insert starts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"sound"
],
"properties": {
"image": {
"type": "string",
"description": "still picture URL; use this or video"
},
"sound": {
"type": "string",
"description": "sound URL (find_sound returns one), or a video whose own audio is used"
},
"video": {
"type": "string",
"description": "video URL to take the picture from, starting at videoStart"
},
"seconds": {
"type": "number",
"description": "fixed insert length, 0.2 to 6 seconds, instead of when the sound lands"
},
"soundEnd": {
"type": "number",
"description": "seconds into the sound where it ends"
},
"soundStart": {
"type": "number",
"description": "seconds into the sound where the insert's sound starts"
},
"videoStart": {
"type": "number",
"description": "seconds into the video where the insert starts"
}
}
}Make a marketplace LISTING SET for ONE product in one call (Amazon, Shopify, Etsy, any store page): the MAIN image on pure white (the product alone filling about 85% of the frame, no text, no props, no added logo), then the secondary images a listing carries — a lifestyle shot, a detail close-up with NO infographic, a scale shot that shows its true size, and the product in use — every one rendered from the product's real photo and held to one visual system (the same light, scale and palette). More shots on request: multi_angle, whats_in_box, back_of_pack, or any shot in your own words. The MAIN renders FIRST and every other image is drawn from it (each waits for it in the queue), so the set matches; pass an approved main as mainImage to redo the rest from it. aplus:true adds Amazon's 7 A+ modules (a 21:9 brand hero and brand promise, 3:2 modules 2-6), each also delivered at its Amazon module's exact pixels, with its text fields suggested only from the product's own data. Priced up front: dryRun:true returns the plan and the exact credits; the charge is that number. Each image is its own queued image job (poll with get_job); they land in the Library. For a batch of ADS use make_static_ads; for one product photo in a scene use plan_ad + generate_image.
| Argument | Type | Description |
|---|---|---|
| copy | any | the seller's own words for A+ text fields, by module id then field, e.g. {aplus_hero:{headline:'…'}}; each is clipped to Amazon's limit. Without it the fields are suggested only from the product's own data, never invented |
| plan | array | the `plan` (or `remaining`) a previous call returned: render exactly those images, no new plan, with the same image |
| aplus | boolean | also Amazon's 7 A+ modules: 1 brand hero and 7 brand promise at 21:9 (Premium full-width 1464x600 + mobile 600x450), 2-6 at 3:2 (key benefits, how to use, materials and details, lifestyle, what you get; Premium image-with-text 800x600, Basic header 970x600). With shots:[] only the A+ modules |
MOTION GRAPHICS and ANIMATED VIDEO in one call: kinetic typography (animated text), a logo animation or intro, a product launch film, an animated social cut. MOVING PICTURES are the default for launch, product and social: in each scene with a picture, one thing physically happens (a seed splits open, an eye opens, the view travels through a forest), filmed by an image-to-video model, with the type, transitions and the music's beat put on top; a few minutes, priced per shot, and with no pictures given each scene paints its own. moving:false, and always for type and logo, makes it in code with no video model: type, pictures and drawn graphics filmed by a moving virtual camera, a 1080p MP4 in about a minute, billed for its compute only. Pick it for 'animated video', 'motion graphics', 'kinetic text', 'text animation', 'logo animation', 'logo intro', 'animated explainer' asks. recipe: 'type' (every line its own animated card, no picture needed), 'logo' (the logo assembles out of dots and settles, the name and tagline land under it), 'launch' (30s 16:9), 'product' (15s), 'social' (15s 9:16). Or `spec`, the whole film as JSON (help:true returns the language). STEPS: without confirm it is a FREE quote that gives both prices (length, the music's tempo; the spec in structuredContent): say both; preview:true is a 1-credit contact sheet of the still version; confirm:true renders. You may write `moves` yourself, or the film's own planner writes them for a few credits. Only the user's or the brand's words appear: never invent a claim, number or review. projectUrl = a past film's url returns its saved spec to edit and render again.
| Argument | Type | Description |
|---|---|---|
| help | boolean | true returns the spec language and recipes; renders nothing |
| hook | string | opening line; '*word*' = italic accent |
| logo | string | logo image: https, upload_file URL or Library id; default the brand's |
CONTENT FACTORY: turn ONE topic into a SERIES of faceless short videos (narration over pictures, no presenter), optionally in the STYLE of a trending reference video. Give `topic` ("make a video about…"), `episodes` (1-10, default 1) or your own `episodeTitles`, `durationSeconds` (20-120 per episode, default 30) and optionally `reference` (a TikTok / Reel / YouTube Short / Facebook / X link or a video file URL whose look, pacing and opening hook every episode copies, with plain subtitles only if the reference burns its own; never its words or characters). Find a trending reference with search_tiktok / search_instagram / search_youtube when the user wants one. THREE STEPS, ALWAYS IN THIS ORDER: (1) call with no `confirm` for a FREE quote of the whole series (per episode and total); (2) optionally `planOnly:true` to see the episode titles and the style card first (a small planning charge); (3) `confirm:true` (pass the `plan` from step 2 to reuse it) queues every episode and returns their job ids; poll get_job. To make MORE of a series later, pass its returned plan as `moreOf`: same style, no episode repeated, the reference not watched again. Each finished episode can be scheduled with this tool again: pass `jobIds` plus `schedule` {channels, at, everyHours, timezone} and it books one post per finished episode, spaced `everyHours` apart (default 24). Captions follow the reference (on if it burns words on screen) unless `captions` says otherwise.
| Argument | Type | Description |
|---|---|---|
| plan | any | the `plan` a planOnly call returned, to run it without planning again |
| style | string | override the look in words (e.g. "flat 2D cartoon") instead of the reference's |
| topic | string | what the series is about, in the user's words |
| voice | string | narrator: 'female' or 'male' (default follows the reference), or any narration voice name make_explainer takes |
Make a BATCH of finished static image ads for one or several products in one call: each ad a genuinely different selling angle (offer, material or ingredient story, how it is used, a lifestyle moment, an objection, proof the brand really has) AND a different layout (editorial still life, poster headline, packshot on colour, in hand, flat lay, split compare, native phone photo and more; never one look repeated across a batch), with its headline, supporting line and CTA printed on the image and the product's real photo and the brand's real logo composited in. `products` names products from the brand's product catalog by id or name (list_products; add one from its page link with add_product; ['all'] takes every product with a photo; omit for the hero product). A product's price is printed only when it was read from its live page within the last hour: older rows are re-read first, and a page that cannot be read means no price on that ad. `count` is ads PER PRODUCT (1-20). `aspectRatios` any of 1:1, 4:5, 9:16 (each ad is composed natively once per ratio, so the ratios are different pictures with the same words; for one picture in several sizes render one ratio and use resize_ad). `brief` steers (a sale, a season, an audience) and is never turned into a price, discount or claim the brand does not state; `angles` names the angles to spread across; `layouts` (free words) replaces the layout set. ALWAYS PRICED FIRST: dryRun:true returns the plan and the quote; then pass that `plan` (and its `batchId`) back to render exactly it without planning again. Renders are QUEUED jobs, at most 12 per call: the rest comes back as `remaining`, to send again as `plan` once some finish. Read each job with get_job; never describe an ad before its URL arrives. For variants of ONE finished ad use headline_variants / resize_ad; to copy a competitor's static use clone_static.
| Argument | Type | Description |
|---|---|---|
| plan | array | the `plan` (or `remaining`) a previous call returned: render exactly those ads, no new planning |
| brief | string | what to lean into: a sale the brand is really running, a season, an audience, a tone |
Use this when the user asks for a quick, low-cost ad or post built from their words and pictures: a slideshow, carousel, iMessage or ChatGPT chat, Notes, kinetic type, myth vs fact, an app tour, or their own HTML design. Rendered from HTML, no AI model, ~30s, billed for its compute only, ready to post. Presets are SHORTCUTS; 'custom' is YOUR OWN design as config.html (+ css), so no layout, type, colour or motion is unsupported. custom: { html, css?, size? ('9:16' default | '4:5' | '1:1' | '16:9' | any 'W:H' | {w,h} px), durationSeconds? (1-60 = VIDEO; CSS/SVG animation and <video> are frame-stepped, scripts stripped), slides?:[{html, css?}] (2-35 = carousel) }; {{logo}} {{brandName}} {{domain}} {{accent}} fill from the brand; images and fonts load by public https URL; notes[] lists what failed to load. YOU write preset copy: short, casual, believable, finished phrases within budget. Preset ids and their fields are listed on `config`. config.music on a VIDEO: omit for a free library bed matched to its mood when the library is stocked (hermoso_capabilities hasMusic; else only its own sound effects), 'off' for silence, or words (a mood or description) to compose one for a flat fee.
| Argument | Type | Description |
|---|---|---|
| config* | object | MUST include config.template: 'custom' or a preset id, plus its fields; language ('Portuguese') for its own words. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4, which takes config.music like any video) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? } is OPTIONAL on every video preset: include it ONLY when the user asked for an end card (a closing card with their logo); without it the video ends on its own last frame. palette and fontStack optional. |
Render a click-driving YOUTUBE / Shorts / Instagram THUMBNAIL or video cover through the full production pipeline (concept, casting, scene, render, tweaks, text), not a bare image prompt. Use it for any "thumbnail", "video cover" or MrBeast-style packaging ask INSTEAD of generate_image. About 9 credits per variant; the headline overlay is free. CONCEPT — open an INFORMATION GAP (the image raises a question the title answers) while staying truthful to the video. Brainstorm ≥5 concepts across the 16 frameworks (ids on `framework`; combining two is fine) before you pick; hermoso_capabilities has each one's 'realize it with' note and the emotion, overlay, font and rim-colour catalogs. THREE GATES, all BEFORE you render: 1. WHO IS IN FRAME — never assume or silently substitute a stranger. A framework with a person and no face photo is refused (nothing charged): ask the user once — themselves (a face photo, identity-locked), a generated person (`castGenericPerson:true`), or a people-free framework. 2. TEXT — default is a CLEAN render with the headline TYPESET over it (free, legible, correctly spelled): pass `headline`. `bakeText:true` only on an explicit ask for words painted INTO the image. Never infer text intent from the topic. 3. HOW MANY — ask once: one, or a SET (offer 4: one concept at different emotions / camera takes). Default 1; `variants` caps at 16. `emotion` is the biggest CTR lever on a face (identity lock is automatic for every face photo). To fix a finished one, re-call with `tweak` + `sourceImage` for a surgical edit (emotion / background / background_color / rim_light) — tweaks chain. ALWAYS check the returned postRenderCheck against the image before presenting it. PROMPT LANGUAGE — write every DESCRIPTIVE field (`sceneBrief`, `keyElements`, `location`, `composition`, `background`, `topic`, each person's `describe`, every `reference`) in ENGLISH, translating the user's words: the models render English better. `headline`, `headlineLines` and `bakedUiText` stay verbatim in the user's language.
| Argument | Type | Description |
|---|---|---|
| font | string | headline font: Anton (default) or any Google Fonts family |
| logo |
Add, rename or delete a tab in a Google Spreadsheet. action:"add" + title · action:"rename" + tab + newTitle · action:"delete" + tab. Name the tab by its TITLE or its numeric sheetId (list_sheet_tabs gives both); an unknown tab is refused with the real list rather than a Google error nobody can map back. DELETING a tab destroys everything on it: call it without confirm first to get the filled-cell count, then confirm:true + confirmCells. Google does not allow removing the LAST remaining tab in a file, and that is refused by name with the way out (clear it, or delete the whole file with delete_drive_file). Every action is read back from the spreadsheet before it is reported as done.
| Argument | Type | Description |
|---|---|---|
| tab | string | which tab — its title or numeric sheetId (rename / delete) |
| title | string | the name for the new tab (action:"add") |
| action* | string | add, rename or delete a tab |
| confirm | boolean | true deletes a tab that holds data; needs confirmCells |
| newTitle | string | what to rename the tab to (action:"rename") |
| sheetUrl | string | Google Sheets link, instead of spreadsheetId |
| confirmCells | number |
Mine ad ANGLES from real customer language: gathers the customer's own words (Reddit, TikTok, the brand's review page + review-site results) and returns a RANKED angle bank — each angle tagged (pain / outcome / identity / fear / competitive-displacement / social-proof / contrast), 2-5 VERBATIM proof quotes, a 0-100 score with breakdown, and a ready-to-run hook in the customer's own voice. Reads YOUR saved brand (pass brandId to target a specific profile — that switches this key's active profile like use_brand). To tear down a COMPETITOR use competitor_teardown instead. YOUR OWN REVIEWS: pass `reviews` (a list of review texts, or one pasted block: one per line, numbered, blank-line separated, or a CSV with a review column) and/or `reviewsUrl` (a CSV, TXT or JSON file from upload_file, or a review page; on a local CLI a file path works too). They are first-class evidence: every quote from them is checked word for word against what you sent and labelled 'your reviews', and a quote that is not verbatim is dropped and counted. useOwnReviewsOnly:true mines only your reviews and gathers nothing public. Limits: 300 reviews, 2,000 characters each, 40,000 in total; over that it is refused at no cost, so send fewer or split into batches. Each angle comes back with `next`: the exact plan_variations and generate_image arguments that turn it into finished statics (one generate_image per angle = statics with distinct angles). Spends credits on the reads and the model's token cost.
| Argument | Type | Description |
|---|---|---|
| brandId | string | a profile id/name from list_brands to mine for; omit to use the active profile |
| reviews | any | your own customer reviews: a list of review texts, or one pasted block (one per line, numbered, blank-line separated, or CSV with a review column) |
| reviewsUrl | string | a URL of your reviews: an uploaded CSV, TXT or JSON file (from upload_file) or a review page. On a local CLI a file path also works. |
MULTIPLY a winning video ad into N variants: each gets a NEW character, outfit, location and/or objects while the cut, the camera motion, the pacing and the ORIGINAL AUDIO stay exactly as they were (that is what made the ad work), and any burned-in captions are removed. Pass the source video URL. HOW: the source video itself DRIVES each variant (motion transfer from one image-edited opening frame), so every variant comes back the SAME LENGTH as the source with the same cut, the same performance and the original audio — only the person, outfit, set and props change. Sources up to 30 seconds work as they are; longer ones are refused for free with the way out (trim it first: post_edit with ops [{op:'trim', start:0, end:30}] — clip_video is the AI highlight clipper, not a trim). Returns the plan and ONE JOB PER VARIANT — call get_job on each until it reports done; do not describe a variant before its URL arrives. COST: quoteOnly:true returns the exact credits free: state that first. A paid planning read (planCredits) runs next, then each variant bills perVariantCredits. DryRun:true pays for that read; pass its `plan` back to render without paying again. Your own pictures: images takes up to 9 of the user's product, people, clothes or places, and change names them @Image1, @Image2 in order ('she holds the bottle in @Image1'); the saved brand's product photo is read as a reference to its real look (useBrand:false leaves it out). A TikTok, Reel, Facebook, X or YouTube post link works as the source: one paid lookup reads it into a file. Quality: 1080p (default) or 720p; quoteOnly prices both. Regions: pass regions:['Berlin','Tokyo'] to restyle variants per market; translation is a separate, explicit step — dub_video on a finished variant. `change` is what should change, in the user's own words ('older women', 'winter streets', 'swap the mug for our bottle'): every variant applies it and the variants still differ around it; a part of it about the voice, words, language, music, length or captions cannot change here, and the reply says so and names the tool that can.
| Argument | Type | Description |
|---|---|---|
| axes | array | what to vary: character, outfit, location, objects (default all four), or your own, e.g. "season" |
action list (default): every sharing link and person with access to a OneDrive file or folder: link URL, view or edit, scope, expiry, password, whether it is inherited from a parent folder. action remove: revoke one (permissionId from the list); anyone relying on it loses access at once and the link cannot be restored, so WITHOUT confirm:true nothing is removed and you get what would go. An inherited permission is removed on the parent folder, and the owner's own access cannot be removed. Removal is verified by reading the list back. 0 credits. Needs OneDrive connected.
| Argument | Type | Description |
|---|---|---|
| action | string | list (default) or remove |
| fileId* | string | OneDrive file or folder id |
| confirm | boolean | required true for remove |
| permissionId | string | for remove |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"action": {
"enum": [
"list",
"remove"
],
"type": "string",
"description": "list (default) or remove"
},
"fileId": {
"type": "string",
"description": "OneDrive file or folder id"
},
"confirm": {
"type": "boolean",
"description": "required true for remove"
},
"permissionId": {
"type": "string",
"description": "for remove"
}
}
}Plan an ad: turns a brand and a product or brief into an ad concept, with copy variants (headline, primary text, call to action) and either an image prompt or a timed video storyboard, plus the recipe and the model ids to render with. It renders nothing; pass its output to render_ad, generate_image or generate_video. When the user names a length ("a 30 second ad", "make it 45s"), pass it as durationSeconds: the storyboard is written to that length and render_ad renders it as one clip or as joined clips. Without it the planner chooses a length that suits the format, so a length the user asked for belongs in durationSeconds rather than only in the brief. The user’s own screenshots or pictures to show inside the video go in `inserts`. Uses credits for the planning step.
| Argument | Type | Description |
|---|---|---|
| hook | string | force the VISUAL scroll-stop mechanic the opening beat is built on — a hook id from list_hooks (e.g. "direct_callout", "mid_problem", "macro_asmr"). Omit to let the planner pick. A hook that cannot be delivered in this brief is DROPPED with the reason rather than rendered wrongly — an on-screen-text hook on an authentic/UGC ad is the one that bites, because that register carries zero on-screen text. |
| brand | any | brand name, or a brand profile object {name,domain,category,palette,products,…}. OMIT to use the workspace’s SAVED brand + memory automatically (see get_brand); use draft_brand to onboard a new one |
| draft | object | Only after a video refusal that offered a lighter draft: the {model, durationSeconds} that refusal named. The plan is then written to that length and priced on that model. Use only the values the refusal gave; when an account cannot cover a video, the refusal comes before planning with three options (an image, adding credits, or this draft when one fits) and the user chooses. |
| format |
Fan a brief into N DISTINCT ad angles (different hooks/mechanics/audiences), each with its own headline + visual brief — then render each with generate_image and rank with score_ad. LLM planning only; renders nothing itself. To plan AND render a batch of finished static ads for one or several products in one call (up to 20 per product), use make_static_ads.
| Argument | Type | Description |
|---|---|---|
| brand | any | brand name or profile object; OMIT to use the workspace’s saved brand |
| count | integer | how many distinct variants (default 6) |
| product* | string | what to advertise |
| language | string | output language for the variant copy (e.g. Spanish) — default English |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"product"
],
"properties": {
"brand": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {},
"additionalProperties": {}
}
],
"description": "brand name or profile object; OMIT to use the workspace’s saved brand"
},
"count": {
"type": "integer",
"maximum": 8,
"minimum": 2,
"description": "how many distinct variants (default 6)"
},
"product": {
"type": "string",
"description": "what to advertise"
},
"language": {
"type": "string",
"description": "output language for the variant copy (e.g. Spanish) — default English"
}
}
}Use this when the user wants a week of social posts planned, or answers yes or no to planning their week. It drafts one batch of 5 posts for the active brand: a new image, a caption and a suggested day and time for each. Without confirm it returns the price and whether the batch fits the account’s posting allowance, and spends nothing. Confirm:true starts the batch and bills credits; read the posts with get_job. Decline:true records that the user said no.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true only after the user agreed to the price |
| decline | boolean | true when the user said no |
| timezone | string | the user’s IANA timezone when known, e.g. America/New_York; the suggested times use it |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"confirm": {
"type": "boolean",
"description": "true only after the user agreed to the price"
},
"decline": {
"type": "boolean",
"description": "true when the user said no"
},
"timezone": {
"type": "string",
"description": "the user’s IANA timezone when known, e.g. America/New_York; the suggested times use it"
}
}
}MECHANICAL post-production on an EXISTING video (URL): ordered primitives run by ffmpeg in seconds, ~2 credits flat, NO AI model, as a NEW video. Ops: a branded end card (adds its seconds), trim, speed (0.5-2x), mute (whole or a window), audio_gain (-20..+6 dB), fade_out, music (a bed UNDER the clip, its own sound kept and ducked under: omit track = a free library track picked by mood, no attribution; or track = an audio link: an upload_file URL, a find_sound or generate_sfx link, a video post's sound; track 'generate' composes it, paid, ONLY when the user asks; start/end, db, replace), voiceover (text spoken OVER it, its sound ducked; ~2.5 words/s; voice female|male; +~1 credit), watermark (brand logo), grain (anti-AI), text (timed words in a native look: style 'tiktok-classic' default / 'clean-minimal' / 'note-style' or a textStyle; start/end, or at:{word} and until:{word} to time them to a spoken phrase, refused when it is never said), join (this video FOLLOWED BY clips[]: Library URLs, direct files or public post links, as one 1080x1920 video, loudness matched). Presets are shortcuts: text/watermark take any x/y, grain any amount, join any ffmpeg transition, a bridge any sound link. 'A viral hook, then our clip' = videoUrl: the hook's post link + [{op:'join', clips:[{url: ours}], bridge}], and it ALWAYS gets a bridge unless the user asks for a bare cut: {kind:'impact'} cuts the hook just before its payoff (found from the footage; cutAt overrides) and lands our clip on a punch-in and flash, with the payoff sound FROM THE HOOK ITSELF: its own audio carries across the cut, else one generated from its frames (up to 8 credits), else a neutral impact. {kind:'text', text, then?} only when the user asks for words over the cut. No voiceover bridge: have our clip's host say the connecting line. matchCut = where our clip starts. ANY OTHER EDIT, op or bridge (whip, zoom, freeze, wipe, split screen, generated shot) is edit_timeline: free keyframes, any size. NEVER generate_video/render_ad for these. Feedback maps to one named move, checked by reading the parameters back, never by re-rendering: quieter/louder (music db, audio_gain), faster/slower (speed; slower = zooms and pans at 0.7x), tighten (trim heads, tails, silences), retime (to a word anchor), recut (order, in-out points), regrade (timeline exposure/contrast/saturation keys).
| Argument | Type | Description |
|---|
Use this when the user asks which of their social posts did best. Aggregate this brand's published posts to answer WHICH HOOKS AND SUBJECTS WORK. Groups by hook (default), subject, format (recipe), channel, media format or posting hour, reports the engagement RATE within each channel, and ranks the best and worst POSTS in each channel. Describe a post by the creative it carried (what it shows, its format, its link), not by its caption — the caption is the least important part of a post. THREE THINGS IT DELIBERATELY WILL NOT DO, and you should repeat them rather than paper over them: (1) it never sums metrics across channels — a LinkedIn impression and a TikTok view are different units, so every comparison is within one channel; (2) it SUPPRESSES a verdict below 5 measured posts and says so, because a confident recommendation from 3 posts is worse than none; (3) a post with no recorded hook (published outside Hermoso, or backfilled without a creation match) counts toward channel and format totals but never votes on which hook works. Present the `finding` verbatim if there is one, and the reason if there is not. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| axis | string | what to group by — default hook; recipe = the format of the creative |
| days | number | look back N days (1-730) over the whole history; omit for the recent posts only |
| brand | string | WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done. |
| channel |
Render an 18-30s music-led PRODUCT SIZZLE: ONE 15s Seedance 2.5 hero clip of the product, diced into fast cuts and intercut with typeset spec/CTA cards on a brand-coloured grain background, mixed to a music bed. Faceless by design — no people, no voiceover, no spoken lines; the cards carry every word, so nothing is left to a video model's spelling. Pass a real packshot as refImage or the label will not be yours. EXPENSIVE — the hero clip is the only paid leg and it is a full 15s Seedance 2.5 render: ≈870 credits at the DEFAULT 1080p, ≈360 at 720p, ≈170 at 480p; a 4k ask renders on Seedance 2.0, the only Seedance tier with 4k, ≈2,360 (call hermoso_capabilities for the live seedance-2.5 per-duration numbers; the dicing and the cards are free, and the music bed is included in these figures). Confirm the spend with the user before calling. For a talking/UGC ad use render_ad or generate_avatar; for a cheap deterministic format use make_template_ad.
| Argument | Type | Description |
|---|---|---|
| cta | string | closing CTA line, ≤30 chars |
| specs | array | up to 4 spec lines for the typeset cards, ≤26 chars each |
| style | string | the LOOK of the sizzle (same price for every look): hypermotion (Fast cuts, crash zooms and speed ramps), product_2d (The real product with playful hand-drawn 2D animation around it), typography (Product framed by huge bold letterforms in the brand colours), dark_minimal (Moody, premium, a single hard light on black), color_pop (Bright saturated colour, glossy, joyful), mixed_media (Editorial collage: paper, cut-outs and texture around the product), saas (For apps and software: floating screens, light trails, particles), product_showcase (Hero commercial: ingredients, splashes and a clean pack shot). Default hypermotion. |
| prompt* | string |
Use this when the user asks which ads a named brand is running. Pull one named brand’s live ads from the Meta (Facebook and Instagram) Ad Library: deduplicated, sorted, with the brand’s own page resolved. One call, back in a few seconds. Use it when the user names a brand and asks what ads it is running ("show me the ads <brand> is running"). Covers the Meta Ad Library only; for a broader search across brands or platforms use research_ads. Spends credits.
| Argument | Type | Description |
|---|---|---|
| url | string | same as domain |
| name | string | same as companyName |
| sort | string | 'longest_running' (default) etc. |
| brand | string | same as companyName |
| limit | number | max ads per platform (default 30) |
| domain | string | the advertiser domain, e.g. liquiddeath.com — a full website URL works (url / website are read as this too). Pass companyName OR domain |
| company | string | same as companyName |
| country | string | 2-letter, default 'US' |
| website | string | same as domain |
Read the ROWS of a Notion database the user shared with Hermoso — e.g. the team's content calendar: each row's title, id, link and every property as a plain value (status, date, channels, caption, links), plus the database's columns with their types and options. Filter with match {"Status": "Scheduled"} (typed from the database's own columns) or a raw Notion filter object; sort with sortBy (a column, or created_time / last_edited_time) and direction. Up to 100 rows per call, cursor for more; the answer says when Notion stopped at its 10,000-result limit. Use it to import a calendar and schedule its posts, or to find the row update_notion_row changes. Read-only, free. Needs Notion connected (Settings > Connectors > Notion).
| Argument | Type | Description |
|---|---|---|
| limit | number | 1-100, default 50 |
| match | object | {"Column": value} — equality for status/select/checkbox/number/date, contains for text/multi-select |
| cursor | string | nextCursor from the previous page of rows |
| filter | object | a raw Notion filter object |
| sortBy | string | column name to sort by, or created_time or last_edited_time |
| archived | boolean | read archived rows instead |
| direction |
Read the text of a Google Doc Hermoso can reach — one it created, or one the user handed over with the Google file picker in the app (that is how an EXISTING doc becomes readable; find its id with list_drive_files). Pass documentId (from create_doc) OR paste a Google Docs URL as docUrl. Under the drive.file scope it reaches nothing else in the user’s Drive; if Google answers that the file was not found, the user has not picked it yet — ask them to pick it in the app rather than retrying. Returns the plain text of EVERY tab (each headed by its tab name when there are several), with smart chips (dropdowns, people, dates, rich links) shown as the text Docs displays, plus `dropdowns[]` — each chip with its title, current value and options, which is what update_doc dropdowns sets. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| docUrl | string | a Google Docs URL to read — the document id is extracted from it |
| documentId | string | the document id (from create_doc) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"docUrl": {
"type": "string",
"description": "a Google Docs URL to read — the document id is extracted from it"
},
"documentId": {
"type": "string",
"description": "the document id (from create_doc)"
}
}
}Read a Notion page the user shared with Hermoso, as markdown (pageId or a Notion link). A database id answers with its data sources instead. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| pageId* | string | page id or Notion link |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"pageId"
],
"properties": {
"pageId": {
"type": "string",
"description": "page id or Notion link"
}
}
}Read cells from a Google Sheet Hermoso can reach — one it created, or one the user handed over with the Google file picker in the app (that is how an EXISTING spreadsheet becomes readable; find its id with list_drive_files). Pass the spreadsheetId (from create_sheet) OR paste a Google Sheets URL as sheetUrl. If Google answers that the file was not found, the user has not picked it yet — ask them to pick it in the app rather than retrying. Returns a 2-D array of values.
| Argument | Type | Description |
|---|---|---|
| range | string | A1 range, e.g. "A1:D50" (default A1:Z1000) |
| sheetUrl | string | a Google Sheets URL to read — the spreadsheet id is extracted from it |
| spreadsheetId | string | the spreadsheet id (from create_sheet) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"range": {
"type": "string",
"description": "A1 range, e.g. \"A1:D50\" (default A1:Z1000)"
},
"sheetUrl": {
"type": "string",
"description": "a Google Sheets URL to read — the spreadsheet id is extracted from it"
},
"spreadsheetId": {
"type": "string",
"description": "the spreadsheet id (from create_sheet)"
}
}
}RECAST A VIRAL HOOK with the brand's own subject (the egg rolling off the table becomes your product; your host does the viral move), then join it into your clip. voice/language: a new voice, or language lip-synced, for one person swapped in. hook: a public post link (TikTok, Reel, Facebook, X, YouTube; one lookup) or a video file. Subject: creator (saved name or id), image (a product or person; a real person's photo records consent), or neither for the brand's first product photo. A DUO OR GROUP (2-4 people): subjects [{creator or image, position: 'left'|'right'|'center' or your own words, refs?, outfit?}], each bound to its place in the frame, keeping that performer's movement and timing on the original sound. LOCATION AND OUTFITS, same pass: location (a place photo or words) moves the scene; outfit (a photo or words) re-dresses the subject, subjects[].outfit each person; with no subject named nobody is swapped and only those change (useBrand:true also swaps in the brand product). Pictures are references, 4 at most; more go in words. Person for person: Seedance 2.5 (720p) if the hook is our render or faceRoute is set, else Wan 3.0 (≤15s; Kling 4.0 Edit past). FULL LENGTH: past its one pass (30 s) the window is recast in passes, each continuing from the previous one's frame and crossfaded across the cut, under the original sound: one video up to 60 s, quoted as passes plus join; the reply names the cut. mode 'auto' is always 'swap' (a region edit: camera, set, light, timing, text and sound kept; a multi-shot hook goes shot by shot); 'motion' only if asked (your person moves in their own photo's setting). swap: your own words for what changes. start/end pick the window (3 s minimum; default: a group or a location/outfit change takes the whole clip, one subject the first 10 s). PAID, QUOTED FIRST: no confirm returns the plan and credits; confirm:true once the user okays it. Then get_job until done, and post_edit with videoUrl = the result and ops [{op:'join', clips:[{url: your clip}], bridge:{kind:'impact'}}].
| Argument | Type | Description |
|---|---|---|
| end | number | window end in seconds of the hook |
| hook* |
Copy the motion of any video (motion transfer): re-perform a clip's motion, camera and timing with YOUR character, product, clothes or place; sound kept. image (+ images, @Image1, @Image2… in prompt) = who and what. engine auto (default): a person clip + ONE photo of a person (saved, real or AI), 4-30s -> seedance (Seedance 2.5 through our face library, 720p: the clip's scene, beats and sound kept, the WHOLE person swapped) when the clip is one of our renders or faceRoute is set; otherwise wan (Wan 3.0, scene kept, 3-15s, 720p), past 15s kling-edit (Kling 4.0 Edit, 3-30s, 1080p by asking); several photos (one per person) -> h3 (H3 Max Recast, ≤4 photos, 5-30s); no person in the clip -> seedance. By name: kling-edit; wan = Wan 3.0 (≤15s, keeps the set); wan-prime; h3; kling = Kling Motion Control (ONE picture, its background becomes the set). voice/language: the new performer gets a new voice (same timing) or speaks another language (lip-synced), in the same job and price. Billed per output second, ~5 min per 5s; dryRun quotes.
| Argument | Type | Description |
|---|---|---|
| tier | string | kling only: 'pro' (default): 1080p and real hand-object interaction. 'standard': about 25% fewer credits and faster, but 720p, and it tends to mime a held object with empty hands. hermoso_capabilities lists the exact credits for both |
| image | string | who performs it: the actor/character image URL (@Image1). Required on kling. No one yet: generate_image a portrait of an AI person from words (no reference photo) and pass its url, or a saved / preset creator’s portrait (list_creators). Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded. |
| video* | string | the reference video whose motion to re-perform |
Reframe a video to a different aspect ratio (e.g. 16:9 master -> 9:16 vertical): the whole original frame is kept and the new canvas around it is generated, with the sound and timing unchanged. Clips up to 300 seconds. Paid render; returns the served URL of the reframed video. Every placement at once (a free blur-pad by default): repurpose_video. A presenter on any video: add_presenter.
| Argument | Type | Description |
|---|---|---|
| video* | string | the source video URL; a URL, not a local file path; use upload_file for local files |
| aspectRatio* | string | the target aspect ratio |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video",
"aspectRatio"
],
"properties": {
"video": {
"type": "string",
"description": "the source video URL; a URL, not a local file path; use upload_file for local files"
},
"aspectRatio": {
"enum": [
"9:16",
"1:1",
"16:9",
"4:3",
"3:4",
"21:9",
"9:21"
],
"type": "string",
"description": "the target aspect ratio"
}
}
}Re-read product pages now (all products with a page, or `ids`) and update the catalog: the live page wins, a failed read is recorded and said. `sync:true` instead imports the whole store catalog (the brand's connected Shopify store, else its website's public catalog), merging into existing rows. Free.
| Argument | Type | Description |
|---|---|---|
| ids | array | product ids to re-read; omit to re-read every product that has a page |
| sync | boolean | import the store catalog instead of re-reading pages |
| brandId | string | a profile id/name from list_brands; omit for the active profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "product ids to re-read; omit to re-read every product that has a page"
},
"sync": {
"type": "boolean",
"description": "import the store catalog instead of re-reading pages"
},
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands; omit for the active profile"
}
}
}Save a durable fact or PREFERENCE about the brand, audience, or the user’s creative TASTE (e.g. “audience is first-time homebuyers”, “prefers bold lime accents”, “always captions off”) into the workspace Memory so it shapes FUTURE ads. For lasting things, not one-off requests. Merges into the existing Memory (never overwrites); de-dupes on identical text. NEVER how Hermoso, a tool, a connector or a platform API behaves (what a call returns, errors, permissions, limits, ids) and never a phone number or email — those are refused.
| Argument | Type | Description |
|---|---|---|
| text* | string | the fact/preference, concise |
| brand | string | profile id/name from list_brands, this call only |
| category | string | short bucket: Brand, Audience, Taste, Do, Don’t, or Preference (default General) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "the fact/preference, concise"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"category": {
"type": "string",
"description": "short bucket: Brand, Audience, Taste, Do, Don’t, or Preference (default General)"
}
}
}The OLD NAME of clone_static, kept so agents that already call it keep working. It is the same tool with the same inputs, result and cost; prefer clone_static.
| Argument | Type | Description |
|---|---|---|
| brandId | string | a profile id/name from list_brands to clone for; omit to use the active profile |
| imageUrl* | string | the URL of the static ad image to clone. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"imageUrl"
],
"properties": {
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands to clone for; omit to use the active profile"
},
"imageUrl": {
"type": "string",
"description": "the URL of the static ad image to clone. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
}
}
}Remove a member from this profile by email — they lose access (you can re-invite them later), AND every connection THEY made on this profile is disconnected with them: their own X, LinkedIn, TikTok, YouTube, Pinterest, Threads… connections (on a channel holding several accounts, only the accounts they added), and any extra login or authorization they added. Connections the owner or anyone else made are never touched, and one with no record of who connected it is treated as the owner's. Queued posts that would publish through their accounts, or that they scheduled themselves, will not go out. The unconfirmed call reports exactly which connections and how many scheduled posts — relay that to the user, then call with confirm:true.
| Argument | Type | Description |
|---|---|---|
| email* | string | the member’s email |
| confirm | boolean | REQUIRED true |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"email"
],
"properties": {
"email": {
"type": "string",
"description": "the member’s email"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
}
}
}Remove one product from the brand's catalog (its ads already made stay in the Library). Free.
| Argument | Type | Description |
|---|---|---|
| id* | string | catalog product id to remove |
| brandId | string | a profile id/name from list_brands; omit for the active profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "catalog product id to remove"
},
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands; omit for the active profile"
}
}
}Use this when the user asks for a video ad, Reel, TikTok ad, UGC video or commercial for their business or product. It renders a finished ad on a real video model, on their brand (real product photos, brand voice), as an mp4 ready to post: timed shots, clean speech, text set in post, no music, captions or end card unless asked. Pass plan_ad’s full output as `creative` (plan_ad turns a one-line ask into the concept). UGC and cinematic plans render from a painted STORYBOARD (a cinematic spot paints a location anchor first, and a hero when someone is on camera); a product-only spot renders from the real product photo, and board:true or foundations:'choose' adds an identity sheet, a moodboard and a board. A plan that fits one clip of the render model renders as one continuous pass; a longer one as STITCHED ACTS (the fewest balanced clips), never time-compressed. Most models cap a clip at 15s and the longest at 30s: dryRun:true shows the act split free, before spending. CAST A SAVED CREATOR with `creator` (list_creators) so the same person stars in every ad; otherwise each render casts a new face. The user’s own pictures (creative.inserts, or `inserts`) are laid in full screen at their seconds. An app shown on a device screen with no real screens to lay there returns needsAppScreens, nothing charged: ask, then pass screenChoice or screens. 1–3 min; poll get_job while it renders. Spends credits.
| Argument | Type | Description |
|---|---|---|
| board | boolean | paint the storyboard board first (default: on for UGC and cinematic spots, off for a product-only commercial); false = render from the text shot list |
| model | string | video model id from hermoso_capabilities (default: the engine the router picks for the plan: seedance-2.5 for every ad, a person talking to camera included). Naming one is a DELIBERATE pick — the server asks before ever swapping it (no silent fallback) |
| music | boolean | string | music bed: OFF unless asked. true = the plan's own music line; or the bed in words ('lo-fi jazz, brushed drums'); false = none |
Report a bug in Hermoso to the team. Use this when something in Hermoso genuinely misbehaves — a tool errors unexpectedly, returns a wrong or malformed result, a render comes back broken, or documented behaviour doesn't match what happened. Include what you were trying to do, the exact tool call and arguments, and what came back. Do NOT use it for out-of-credits, a policy refusal, or a missing capability (use request_feature for that). Free, no credits.
| Argument | Type | Description |
|---|---|---|
| details* | string | what you were doing, the tool + arguments you called, what you expected, and what actually happened (paste the exact error) |
| summary* | string | one-line summary of the bug |
| severity | string | high = blocks the task or loses paid work; medium = wrong output but workable; low = cosmetic |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"summary",
"details"
],
"properties": {
"details": {
"type": "string",
"description": "what you were doing, the tool + arguments you called, what you expected, and what actually happened (paste the exact error)"
},
"summary": {
"type": "string",
"description": "one-line summary of the bug"
},
"severity": {
"enum": [
"low",
"medium",
"high"
],
"type": "string",
"description": "high = blocks the task or loses paid work; medium = wrong output but workable; low = cosmetic"
}
}
}ONE finished static ad in, VARIANTS × FORMATS out, in one job, as a named file list (<name>_<variant>_<format>_<WxH>.png) with a manifest. The ad's text is read ONCE as the source of truth; each variant's change is made ONCE on the master; every format is derived from that master; every output is text-checked line by line and any line it lost is named (never re-rendered). Variants: {headline}, {language}, {swap:[{from,to}]} (exact words: a price, an offer, a CTA) or {edit} (any change in words), each with an optional {name}; the original is always included. Formats: default 1:1, 4:5, 9:16, 16:9, or 'everywhere', any canvas or a platform word (snapped to the nearest canvas an image model makes). At most 24 renders. dryRun:true = the exact price, which is what it charges. One resize alone is resize_ad.
| Argument | Type | Description |
|---|---|---|
| name | string | the asset name the files are named after |
| image* | string | the ad: an https / upload_file URL, a Library id (L0…), or a local path on the CLI |
| dryRun | boolean | price only: nothing made, nothing charged |
| formats | array | default ['1:1','4:5','9:16','16:9'] |
| fixLabel | boolean | false = no product-label check (default on when the ad shows the brand's saved product) |
| variants | array | the versions besides the original; a bare string is an edit in words |
Turn a VIDEO into WRITTEN pieces: a summary, YouTube chapters (first at 0:00, at least 3, each 10s+), chapter summaries, an X thread (every post under 280 characters), a blog post, timestamped verbatim quotes, or any piece in your own words (pass the words as a format). The video: a YouTube link (read from its captions), a video file URL, a Library id, an upload_file URL or a social post link (read by watching it); or the text itself, when you already have it from analyze_video (which is the tool that just reads a video back). Several formats share ONE read. Priced by the video's length before anything is spent: without confirm it returns the exact credits and spends nothing; confirm:true writes and charges exactly that. Only the paid minutes are read. Every piece is written only from what the video says. Built-in skills youtube_script and retention_skeleton (get_skill) write long-form scripts.
| Argument | Type | Description |
|---|---|---|
| read | string | 'watch' reads a YouTube video by watching it rather than from its captions (one with no captions) |
| model | string | a Claude writing-model id from hermoso_capabilities; omit for the default |
| video | string | a YouTube link, a video file URL, a Library id (L0…), an upload_file URL or a social post link |
| confirm | boolean | true only after the user agreed to the quoted price |
| formats | array | summary, chapters, chapter_summaries, thread, blog, quotes, or any piece in your own words; default summary |
| seconds | number |
ONE video in, EVERY placement out, in one job, as a named file list (<name>_<placement>_<WxH>[_<lang>].mp4) with a manifest. Placements: 'everywhere' (default: 9:16, 4:5, 1:1, 16:9), any W:H, a pixel size (1200x628) or a platform word (tiktok, reels, stories, feed, square, youtube, linkedin, pinterest). Each is reframed by a BLUR-PAD by default (the whole frame over a blurred copy of itself; free, no AI), reframe 'crop' (fill, edges cut; free) or 'ai' (the new canvas generated around the frame; paid, for a canvas the engine makes, else padded and said). A placement may be {placement, reframe} to mix. Optional captions (word-timed, ~4 words a cue, placed for each canvas) and languages (a lip-synced dub per language; every placement of each). A pack of pads and crops only is billed 0. dryRun:true = the exact price (the sum of every leg's own price), which is what it charges. For ONE canvas use post_edit op pad.
| Argument | Type | Description |
|---|---|---|
| pad | object | the pad's look: fill 'blur' (default) or a #rrggbb colour, blur 0-1, dim 0-0.6, position top/center/bottom or 0-1 |
| name | string | the asset name the files are named after |
| video* | string | the video: an https / upload_file URL, a Library id (L0…), or a local path on the CLI |
| dryRun | boolean | price only: nothing made, nothing charged |
| reframe | string | default 'pad' (free); 'ai' is paid and only when the user asks |
| captions | any | true = word-timed captions on every file (only when the user asks); a textStyle preset name or object sets the look |
Ask the Hermoso team for a capability that doesn't exist yet. Use this when you need something Hermoso genuinely can't do — an unsupported platform or channel, a missing model, an export format, a tool that would have completed the user's task but isn't available. Say what the user was trying to achieve, not just the feature name — the use case is what gets built. Free, no credits.
| Argument | Type | Description |
|---|---|---|
| details* | string | what the user was actually trying to achieve, why the current tools couldn't do it, and what you'd expect the capability to do |
| summary* | string | one line: the capability you need |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"summary",
"details"
],
"properties": {
"details": {
"type": "string",
"description": "what the user was actually trying to achieve, why the current tools couldn't do it, and what you'd expect the capability to do"
},
"summary": {
"type": "string",
"description": "one line: the capability you need"
}
}
}Use this when the user asks an open-ended ad question across several brands or platforms, such as comparisons, which angles are working in a market, or ad examples for a category. Do not use for one named brand’s current ads: pull_competitor_ads covers that. Open-ended ad research that needs JUDGMENT across platforms — comparisons, "what angle is working", "who else is doing this", anything where the right sources are not known up front. It is an agentic loop (several rounds of library pulls plus a written synthesis) and typically takes 30-60 seconds, so it is the WRONG tool for a question that names its own answer. For one named brand’s live ads use pull_competitor_ads; for one keyword or one advertiser on Meta use search_meta_ads — both are a single call and return in a few seconds. Spends credits — an agentic loop, so a handful rather than the one-call cost of a targeted search.
| Argument | Type | Description |
|---|---|---|
| brand | any | brand name or profile object to tailor the research to; omit to use the workspace’s saved brand |
| query* | string | what to research, e.g. "the longest-running protein-pancake ads on Meta" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"brand": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {},
"additionalProperties": {}
}
],
"description": "brand name or profile object to tailor the research to; omit to use the workspace’s saved brand"
},
"query": {
"type": "string",
"description": "what to research, e.g. \"the longest-running protein-pancake ads on Meta\""
}
}
}Re-lay out ONE finished static ad for other placements: the same ad, product, copy (word for word), logo and style, recomposed natively for each canvas rather than cropped. Pass `image` and optionally `aspectRatios` from 1:1, 4:5, 9:16, 16:9, 3:4, 4:3 (default 1:1, 4:5 and 9:16; the ad's own ratio is skipped). Reads the ad's text first (3 credits) so every line survives, then one image edit per canvas. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. Priced before it runs. Several sizes AND versions (headlines, languages, offers) at once: repurpose_image. For VIDEO use reframe_video.
| Argument | Type | Description |
|---|---|---|
| image* | string | the finished static ad: URL, Library item URL, upload_file URL or local path |
| fixLabel | boolean | false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product) |
| aspectRatios | array | target canvases (default 1:1, 4:5, 9:16) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"image"
],
"properties": {
"image": {
"type": "string",
"description": "the finished static ad: URL, Library item URL, upload_file URL or local path"
},
"fixLabel": {
"type": "boolean",
"description": "false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)"
},
"aspectRatios": {
"type": "array",
"items": {
"enum": [
"1:1",
"4:5",
"9:16",
"16:9",
"3:4",
"4:3"
],
"type": "string"
},
"description": "target canvases (default 1:1, 4:5, 9:16)"
}
}
}RESTYLE a clip into a new LOOK: every frame redrawn, shots, motion, camera, framing, timing and sound kept. Looks: `Restyle looks` in hermoso_capabilities (claymation, knitted yarn, cel-shaded CG anime…), your own words, or a look BUILT FROM YOUR IMAGES (styleImages); characters draws your own characters in. engine auto (default) picks by what is in the clip: a real-looking person -> kling (Kling 4.0 Edit, 3-15s); none -> wan (Wan 3.0, recommended, ≤15s) else seedance (Seedance 2.5, 4-30s); wan-prime = Wan 3.0 Prime by name (costs more). Paid; dryRun quotes it. One change only: edit_video.
| Argument | Type | Description |
|---|---|---|
| style | string | a look id from hermoso_capabilities (e.g. claymation, knitted-yarn, cel-shaded-anime-cg) |
| video* | string | the source video URL (a render, job result or list_library) |
| engine | string | default auto |
| describe | string | a look in the user’s own words, when no preset fits, or extra detail on top of a preset |
| faceRoute | string | 'face_lane' = the user's "I own the rights to this face" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own. |
| characters | array | characters to draw the people as. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded. |
Finish a video-blocks explainer or faceless-channel film (make_explainer) that STOPPED partway: a block the video model would not render, or a server restart. What it already made is kept (the script, the style key, the narration and every block that rendered), so this renders ONLY the missing blocks and re-assembles the same film, with the same topic, look, voice, length and options. It reserves credits only for what is missing and never charges again for what was kept. get_job and list_jobs show which jobs can be resumed and which blocks are kept. Quote it with dryRun first and tell the user the number. A finished film is not resumed: to redo one block of a finished film, use fix_beat with its job id.
| Argument | Type | Description |
|---|---|---|
| jobId* | string | the id of the film that stopped (job_...), as get_job or list_jobs shows it |
| dryRun | boolean | true = return the credits the resume reserves and render nothing |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"jobId"
],
"properties": {
"jobId": {
"type": "string",
"description": "the id of the film that stopped (job_...), as get_job or list_jobs shows it"
},
"dryRun": {
"type": "boolean",
"description": "true = return the credits the resume reserves and render nothing"
}
}
}Add a portrait to this workspace’s reusable CAST so the SAME person can star in future ads — the headless twin of the app’s + > Pick a creator > save. Pass the portrait’s public url (a generate_image render of an AI person, or a photo of a real person you have permission to use, or of yourself; never a photo just because it is public) plus a name to call them by; from then on list_creators returns them and their url can be re-passed to generate_avatar / generate_video / recast_motion. Saving is FREE and renders nothing. LIKENESS: leave `source` "generated" for an AI-made person (free on every plan) and use "upload"/"social" for a REAL person. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.
| Argument | Type | Description |
|---|---|---|
| look | string | their canonical wardrobe/appearance in words — reused to hold the look steady across ads |
| name* | string | what to call this creator (e.g. “Sarah”) — list_creators and the app’s picker match on it |
| brand | string | profile id/name from list_brands, this call only |
| image | string | REQUIRED except with useAnyway. public https url of the portrait (an existing render’s url, or any public photo). Not a local file path — upload it with upload_file first and save the url that returns |
| poses | array | up to 4 extra full-body / angle plates of the SAME person (public urls) — they make a wider shot hold the identity |
Save a reusable PLAYBOOK — the strategy takeaways worth re-running: the hooks that work, the angles, the formats, and the concrete plays. Use it to keep what a competitor_teardown or mine_angles just found, or to bank a creative you want to repeat. Lands in the same Playbooks library the web app lists, runs and manages. Distinct from save_skill (a directive applied to every ad) and from the swipefile (raw saved creative). Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | the playbook headline — what it is, in a few words |
| brand | string | profile id/name from list_brands, this call only |
| hooks | array | the opening hooks worth reusing, verbatim |
| plays | array | the concrete plays to run ({title, detail}) — the actionable half |
| angles | array | the persuasion angles ({title, detail}) |
| source | string | where it came from, e.g. “teardown · Ridge” |
| formats | array | the formats/recipes this plays best in (e.g. ugc_selfie, cinematic, static) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "the playbook headline — what it is, in a few words"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"hooks": {
"type": "array",
"items": {
"type": "string"
},
"description": "the opening hooks worth reusing, verbatim"
},
"plays": {
"type": "array",
"items": {
"type": "object",
"required": [
"title"
],
"properties": {
"title": {
"type": "string",
"description": "the play to run, in a few words"
},
"detail": {
"type": "string",
"description": "how to run the play; optional, up to 600 characters"
}
},
"additionalProperties": {}
},
"description": "the concrete plays to run ({title, detail}) — the actionable half"
},
"angles": {
"type": "array",
"items": {
"type": "object",
"required": [
"title"
],
"properties": {
"title": {
"type": "string",
"description": "the angle, in a few words"
},
"detail": {
"type": "string",
"description": "what the angle is and why it persuades; optional, up to 600 characters"
}
},
"additionalProperties": {}
},
"description": "the persuasion angles ({title, detail})"
},
"source": {
"type": "string",
"description": "where it came from, e.g. “teardown · Ridge”"
},
"formats": {
"type": "array",
"items": {
"type": "string"
},
"description": "the formats/recipes this plays best in (e.g. ugc_selfie, cinematic, static)"
}
}
}Save a reusable custom SKILL — a named creative directive/playbook applied to future ads (a hook formula, a UGC recipe, a compliance rule, a named specialist persona like “our founder-story style” or “short-form ad strategist”). Distill an imperative, self-contained directive. Merges into the workspace Skills library (list_skills shows built-ins + your custom skills).
| Argument | Type | Description |
|---|---|---|
| name* | string | short skill name, e.g. “Founder-story hook” |
| brand | string | profile id/name from list_brands, this call only |
| directive* | string | the full instruction the skill applies when used (1–6 sentences, imperative) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"directive"
],
"properties": {
"name": {
"type": "string",
"description": "short skill name, e.g. “Founder-story hook”"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"directive": {
"type": "string",
"description": "the full instruction the skill applies when used (1–6 sentences, imperative)"
}
}
}Save a Hermoso render — or ANY file — into the user’s connected Google Drive. Pass a Hermoso render URL as url (or urls[] for several); for a local/external file, call upload_file first and pass the url it returns. Optional folder (created if new) + name. Returns the Drive file(s) with a webViewLink. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets and Docs). NOTE: Hermoso uses the drive.file scope, so it reaches ONLY the files it created plus any the user explicitly handed over with the Google file picker in the app — never their whole Drive.
| Argument | Type | Description |
|---|---|---|
| url | string | a single Hermoso render URL to save |
| name | string | file name (single save) |
| urls | array | several render URLs (up to 20) to save in one call |
| folder | string | Drive folder name to save into (created if new) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "a single Hermoso render URL to save"
},
"name": {
"type": "string",
"description": "file name (single save)"
},
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "several render URLs (up to 20) to save in one call"
},
"folder": {
"type": "string",
"description": "Drive folder name to save into (created if new)"
}
}
}Save to the user's Notion as a NEW page: a research report, competitor teardown or campaign report as `content` (markdown: headings, lists, links, and an image or video URL on its own line becomes the media), a swipefile collection (swipefileCollection: name or id; each ad with its creative, copy, run dates and link), the content calendar (includeSchedule:true), or a mix. Put it under parentPageId, as a row in databaseId, or omit both for a private page in their workspace. Long text is split across blocks, never cut. The answer is read back from Notion. Free; never overwrites anything.
| Argument | Type | Description |
|---|---|---|
| icon | string | one emoji |
| title | string | title of the new Notion page |
| content | string | markdown body |
| databaseId | string | database id or link: saves as a new row |
| dataSourceId | string | data source id to save into as a new row; pass only one of parentPageId, databaseId, dataSourceId |
| parentPageId | string | page id or link to save under |
| includeSchedule | boolean | add the scheduled posts |
| swipefileCollection | string | swipefile collection name or id |
Save a Hermoso render — or ANY file — into the user’s connected Microsoft OneDrive. Pass a Hermoso render URL as url (or urls[] for several); for a local/external file, call upload_file first and pass the url it returns. Optional folder (created if new) + name. Returns the OneDrive file(s) with a webViewLink. Needs OneDrive connected (Settings > Connectors > OneDrive).
| Argument | Type | Description |
|---|---|---|
| url | string | a single Hermoso render URL to save |
| name | string | file name (single save) |
| urls | array | several render URLs (up to 20) to save in one call |
| folder | string | OneDrive folder name to save into (created if new) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "a single Hermoso render URL to save"
},
"name": {
"type": "string",
"description": "file name (single save)"
},
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "several render URLs (up to 20) to save in one call"
},
"folder": {
"type": "string",
"description": "OneDrive folder name to save into (created if new)"
}
}
}Save one or more ads/creatives to a named SWIPEFILE collection, creating the collection if it does not exist — the headless twin of the heart on every ad card in the web app. Use it whenever research turns up something worth keeping: a competitor ad from search_meta_ads / pull_competitor_ads, an organic post, or one of your own renders. Saved ads persist to the workspace board the web Swipefile tab shows, and feed the taste signal every future ad is planned against. De-dupes: re-saving the same ad (same key/link/media) MOVES it into the named collection instead of duplicating it. Free.
| Argument | Type | Description |
|---|---|---|
| brand | string | profile id/name from list_brands, this call only |
| items* | array | the ads to save |
| collection* | string | the collection name — an existing one, or a new one to create |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"collection",
"items"
],
"properties": {
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "a stable id for this ad if you have one (an ad_archive_id, creativeId, …). Omit and one is derived from the link/media so re-saving is idempotent"
},
"body": {
"type": "string",
"description": "the ad copy"
},
"link": {
"type": "string",
"description": "link to the ad in its library / the destination URL"
},
"image": {
"type": "string",
"description": "image URL"
},
"title": {
"type": "string",
"description": "headline / hook"
},
"video": {
"type": "string",
"description": "video URL"
},
"pageName": {
"type": "string",
"description": "alias of advertiser"
},
"platform": {
"type": "string",
"description": "where it ran — 'meta', 'google', 'linkedin', 'tiktok', 'generated', …"
},
"page_name": {
"type": "string",
"description": "alias of advertiser: the field search_meta_ads returns, accepted as-is"
},
"advertiser": {
"type": "string",
"description": "the brand running the ad"
}
},
"additionalProperties": {}
},
"minItems": 1,
"description": "the ads to save"
},
"collection": {
"type": "string",
"description": "the collection name — an existing one, or a new one to create"
}
}
}Virality/performance prediction for a finished ad (image or video URL): overall score, per-dimension breakdown (scroll-stop, hook, clarity, brand/product, CTA, retention, goal fit, own: could another brand run it unchanged), strengths, and the single biggest fix. Use BEFORE spending on distribution, or to rank variants.
| Argument | Type | Description |
|---|---|---|
| url* | string | the ad asset URL (a /generated/ path or public URL) |
| kind | string | 'image' (default) or 'video' |
| intent | string | what the ad is trying to achieve, for goal-fit scoring |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "the ad asset URL (a /generated/ path or public URL)"
},
"kind": {
"enum": [
"image",
"video"
],
"type": "string",
"description": "'image' (default) or 'video'"
},
"intent": {
"type": "string",
"description": "what the ad is trying to achieve, for goal-fit scoring"
}
}
}Structured Google Ads Transparency pull for ONE advertiser (by domain or advertiserId) — use when you know the brand; use research_ads for open-ended research. Deliberately fetches the cheap BASIC listing (get_ad_details=false, ~1 credit — the detailed variant with per-ad headlines costs 25 credits/call and is not exposed here). Returns compact JSON {advertiser, format, adUrl, image, firstShown, lastShown} per ad.
| Argument | Type | Description |
|---|---|---|
| limit | integer | max ads returned (1–25, default 8) |
| domain | string | the advertiser's domain, e.g. nike.com |
| region | string | 2-letter region, default US |
| advertiserId | string | Google advertiser id (AR…) when the domain is ambiguous |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "max ads returned (1–25, default 8)"
},
"domain": {
"type": "string",
"description": "the advertiser's domain, e.g. nike.com"
},
"region": {
"type": "string",
"description": "2-letter region, default US"
},
"advertiserId": {
"type": "string",
"description": "Google advertiser id (AR…) when the domain is ambiguous"
}
}
}Organic Instagram REELS keyword search (/v2/instagram/reels/search — our only IG keyword surface; profile/hashtag pulls go through fetch_social_data with a handle). Returns compact JSON {desc, author, handle, plays, likes, link, cover} per reel, ranked by plays. It matches captions worldwide and cannot be limited to a country or language: for one market, search in the words people there use, or read named local accounts' Reels through fetch_social_data's Instagram profile endpoint, which is the better first step for a local market. Spends about a credit.
| Argument | Type | Description |
|---|---|---|
| limit | integer | max reels returned (1–25, default 8) |
| query* | string | keyword to search reels for |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"limit": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "max reels returned (1–25, default 8)"
},
"query": {
"type": "string",
"description": "keyword to search reels for"
}
}
}Structured LinkedIn Ad Library search by company name, keyword, or companyId — use for a targeted B2B pull; use research_ads for open-ended research. Returns compact JSON {advertiser, headline, description, cta, link, media, dates, impressions} per ad — LinkedIn's library includes impression counts. Spends about a credit.
| Argument | Type | Description |
|---|---|---|
| limit | integer | max ads returned (1–25, default 8) |
| company | string | advertiser company name |
| keyword | string | keyword across all advertisers |
| companyId | string | LinkedIn company id (numeric) when the name is ambiguous |
| countries | string | CSV of 2-letter codes like 'US,CA'; omit or 'ALL' = worldwide |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "max ads returned (1–25, default 8)"
},
"company": {
"type": "string",
"description": "advertiser company name"
},
"keyword": {
"type": "string",
"description": "keyword across all advertisers"
},
"companyId": {
"type": "string",
"description": "LinkedIn company id (numeric) when the name is ambiguous"
},
"countries": {
"type": "string",
"description": "CSV of 2-letter codes like 'US,CA'; omit or 'ALL' = worldwide"
}
}
}Structured Meta (Facebook/Instagram) Ad Library pull — use when you know exactly WHAT to fetch: a keyword (query) OR one advertiser (companyName / pageId). Returns compact JSON {page_name, body, cta, link, dates, media} per ad. For open-ended research that needs judgment across platforms, use research_ads instead. Spends credits on each ad-library read it makes: one for a keyword; for a brand name, the page lookup and its ads (plus a keyword search when that page has none).
| Argument | Type | Description |
|---|---|---|
| limit | integer | max ads returned (1–25, default 8) |
| query | string | keyword search across ALL advertisers (use INSTEAD of companyName/pageId) |
| domain | string | the advertiser’s website, e.g. buffer.com — with companyName, used to confirm which Meta page is really theirs |
| pageId | string | one advertiser’s ads by Facebook page id (most precise) |
| status | string | ACTIVE = currently running; default ALL (includes proven past winners) |
| country | string | 2-letter code or 'ALL' (default ALL) |
| mediaType | string | filter by creative type (default ALL) |
| companyName | string | one advertiser’s ads by brand name — resolved to the brand’s own page only on evidence (its Instagram, its domain, or ads that land on its domain); pass domain too for the best match |
Find a PLACE to tag on an Instagram or Facebook post: a shop, cafe, restaurant, venue or landmark. Give its name with the city ("Blue Bottle Coffee Oakland"), or paste its Facebook Page link, an Instagram place link (instagram.com/explore/locations/...) or its numeric id. A pasted link or id is read and comes back as that Page's taggable pageId with its name and address, or a plain reason it cannot be tagged. Returns a handful of matching Facebook Pages that have an address: pageId, name, address, category, link. These are matches to CONFIRM with the user (check the name and address; a place tag is public), and Instagram checks the place when the post is made. Needs no connected account. Spends about 1 credit per Page it checks (a name search checks up to 5 Pages, so about 5 to 7 credits); a pasted Facebook Page link or numeric id is about 1 credit; an Instagram place link costs nothing.
| Argument | Type | Description |
|---|---|---|
| near | string | optional city or area to narrow a name search, e.g. "Paris" |
| limit | integer | how many Pages to check on a name search, 1 to 8 (default 5); each is about 1 credit |
| query* | string | the place name with its city (e.g. "Blue Bottle Coffee Oakland"), or a Facebook Page link, an Instagram place link, or a numeric id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"near": {
"type": "string",
"description": "optional city or area to narrow a name search, e.g. \"Paris\""
},
"limit": {
"type": "integer",
"maximum": 8,
"minimum": 1,
"description": "how many Pages to check on a name search, 1 to 8 (default 5); each is about 1 credit"
},
"query": {
"type": "string",
"description": "the place name with its city (e.g. \"Blue Bottle Coffee Oakland\"), or a Facebook Page link, an Instagram place link, or a numeric id"
}
}
}The POSTS people make ABOUT a subject — a brand ("liquid death"), a product, a hobby ("coffee"), a hashtag ("#homecafe") — from whoever posted them, across organic TikTok, Instagram Reels and YouTube in ONE call, ranked by views. Not the brand's own ads (search_meta_ads / research_ads) and not the people (find_creators folds these same posts into creators): use it to see what is actually being posted and watched about a subject, to find clips worth cloning (clone_video), and to read the hooks and angles an audience already responds to. About one credit per platform searched (one query each by default; `queries` adds "best X" / "X review" / #tag variants, each a paid call); repeats inside 20 minutes are free.
| Argument | Type | Description |
|---|---|---|
| limit | number | posts per platform, 1–60 (default 24) |
| topic* | string | subject, brand, product or hashtag — "liquid death", "coffee", "#homecafe" |
| queries | number | query variants per platform, 1–4 (default 1); each is a paid search call |
| platforms | array | default all three |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"topic"
],
"properties": {
"limit": {
"type": "number",
"description": "posts per platform, 1–60 (default 24)"
},
"topic": {
"type": "string",
"description": "subject, brand, product or hashtag — \"liquid death\", \"coffee\", \"#homecafe\""
},
"queries": {
"type": "number",
"description": "query variants per platform, 1–4 (default 1); each is a paid search call"
},
"platforms": {
"type": "array",
"items": {
"enum": [
"tiktok",
"instagram",
"youtube"
],
"type": "string"
},
"description": "default all three"
}
}
}Reddit keyword search (/v1/reddit/search, top-ranked) — a goldmine for the customer's OWN words (pain points, objections, language) to mine into ad hooks and copy. Returns compact JSON {desc (title+selftext), subreddit, upvotes, comments, link} per post. Spends about a credit.
| Argument | Type | Description |
|---|---|---|
| limit | integer | max posts returned (1–25, default 8) |
| query* | string | what to search Reddit for |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"limit": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "max posts returned (1–25, default 8)"
},
"query": {
"type": "string",
"description": "what to search Reddit for"
}
}
}Organic Threads keyword search (/v1/threads/search) — short-form text/social posts for trend + voice research. Returns compact JSON {desc, author, handle, likes, link, cover} per post. Spends about a credit.
| Argument | Type | Description |
|---|---|---|
| limit | integer | max posts returned (1–25, default 8) |
| query* | string | keyword to search Threads for |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"limit": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "max posts returned (1–25, default 8)"
},
"query": {
"type": "string",
"description": "keyword to search Threads for"
}
}
}Organic TikTok keyword search (there is NO TikTok ad library) — top-performing videos to mine for hooks/trends/remixable creative. Returns compact JSON {desc, author, handle, plays, likes, link, cover} per video, ranked by plays. Use research_ads for open-ended research; for what to POST, tiktok_ideas. Spends about a credit.
| Argument | Type | Description |
|---|---|---|
| limit | integer | max videos returned (1–25, default 8) |
| query* | string | keyword or hashtag (no # needed) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"limit": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "max videos returned (1–25, default 8)"
},
"query": {
"type": "string",
"description": "keyword or hashtag (no # needed)"
}
}
}Organic YouTube keyword search (/v1/youtube/search) — videos to mine for hooks/angles/long-form structure. Returns compact JSON {desc (title), author, handle, plays, link, cover} per video, ranked by views. Spends about a credit.
| Argument | Type | Description |
|---|---|---|
| limit | integer | max videos returned (1–25, default 8) |
| query* | string | keyword to search videos for |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"limit": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "max videos returned (1–25, default 8)"
},
"query": {
"type": "string",
"description": "keyword to search videos for"
}
}
}Turn automatic credit reloads on or off (admin only): when the balance drops below a threshold, the card on file is charged for a top-up pack — SERVER-SIDE, even with no app open. Requires a saved card, added once in the app at first checkout/top-up; if there's none the tool tells you exactly where to add it. After that one-time card setup, agents can manage auto-reload, top-ups and plan links fully. Members (read-only billing) get an 'ask an admin' message.
| Argument | Type | Description |
|---|---|---|
| enabled* | boolean | true to turn auto-reload on, false to turn it off |
| reloadCredits | integer | how many credits to add each reload — must match a credit pack size (see buy_credits) |
| thresholdCredits | integer | reload when the balance drops below this many credits |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"enabled"
],
"properties": {
"enabled": {
"type": "boolean",
"description": "true to turn auto-reload on, false to turn it off"
},
"reloadCredits": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "how many credits to add each reload — must match a credit pack size (see buy_credits)"
},
"thresholdCredits": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "reload when the balance drops below this many credits"
}
}
}Set, change or stop a brand's MARKET RADAR (the standing competitor watch; Ad Spy > Watching in the web app; each brand has its own), run weekly or daily: newAds (what watched brands launched), winners (their ad passing 30 days running), trends + topPosts (formats and most-viewed posts for niche topics), ideas (3 to make). Only what you pass changes. competitors / topics REPLACE their lists (max 8 brands, 5 topics); both empty stops it and clears findings. auto:true picks 3-5 competitors, 2-3 similar brands and 2-3 topics from the saved brand or `seed` (one model call, billed at its token cost). dryRun:true quotes one check, saves nothing. Saving is free; each check holds its quote first and is skipped when the balance is short. runNow is refused while a check runs or just after one finished. Read it with list_watch_findings.
| Argument | Type | Description |
|---|---|---|
| auto | boolean | true picks competitors, similar brands and topics from the saved brand or seed; bills one model call at its token cost |
| seed | string | for auto: a website, brand or niche |
| brand | string | profile id/name from list_brands, this call only |
| boolean | the weekly email | |
| force | boolean | with runNow: check again right after a check, allowed once what is watched changed |
| dryRun | boolean | true quotes one check and saves nothing |
Set WHICH of a connector's accounts this profile is allowed to post to and spend from — Facebook Pages / Instagram / Meta ad accounts, Google Ads customers, LinkedIn company Pages (and the personal profile), Pinterest or Microsoft Advertising ad accounts. Pass ids from list_connector_accounts. This REPLACES the current selection: anything you leave out is un-shared, and an EMPTY list shares nothing (publishing then refuses — it fails closed by design, and the server re-verifies every id against the live connection, so an id the account cannot actually reach is rejected rather than saved). Ask the user which accounts they mean; posting as the wrong Page is a public mistake. Providers: tiktok, x, youtube, threads, bluesky, telegram, pinterest, instagram, meta, google_ads, linkedin, pinterest_ads, linkedin_ads, reddit_ads, apple_ads, microsoft_ads, google_business, google_analytics, snapchat_ads, x_ads, tiktok_ads, google_tag_manager, google_search_console, bing_webmaster. Free.
| Argument | Type | Description |
|---|---|---|
| provider* | string | which connector to scope |
| accountIds* | array | the ids (from list_connector_accounts) this profile may use — an empty array shares nothing |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider",
"accountIds"
],
"properties": {
"provider": {
"enum": [
"tiktok",
"x",
"youtube",
"threads",
"bluesky",
"telegram",
"pinterest",
"instagram",
"meta",
"google_ads",
"linkedin",
"pinterest_ads",
"linkedin_ads",
"reddit_ads",
"apple_ads",
"microsoft_ads",
"google_business",
"google_analytics",
"snapchat_ads",
"x_ads",
"tiktok_ads",
"google_tag_manager",
"google_search_console",
"bing_webmaster"
],
"type": "string",
"description": "which connector to scope"
},
"accountIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "the ids (from list_connector_accounts) this profile may use — an empty array shares nothing"
}
}
}Lock an image as the ad's real PRODUCT photo and SAVE it as this brand's default product, so every later plan_ad / render_ad / generate_image grounds on the true packaging without being told again. Pass `imageUrl` = a product shot's URL — an image from a prior research result (an organic Instagram/TikTok post, a scraped page image), a workspace / list_product_photos url, or any public product photo. The server downloads it and runs a product+safety check: a lifestyle/scene shot with no clear product, or an off-category / unsafe image, is REJECTED and NOTHING is locked or saved (the summary says why). On PASS it persists the photo to a DURABLE url, writes it to the brand's product library as the DEFAULT, and READS THE BRAND BACK to confirm — `savedToBrand` and the summary report what the brand ACTUALLY holds now, never what was asked for, so if it did not become the default you are told instead of finding out from a paid render. Bills one vision check. Reads YOUR saved brand for the category match (pass brandId to target a specific profile — switches this key's active profile like use_brand).
| Argument | Type | Description |
|---|---|---|
| brandId | string | a profile id/name from list_brands to lock the product for; omit to use the active profile |
| imageUrl* | string | the image URL to lock as the product (from a research result, a workspace / list_product_photos url, or any public product photo) |
| source_note | string | a short note on where it came from, e.g. "from their IG post" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"imageUrl"
],
"properties": {
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands to lock the product for; omit to use the active profile"
},
"imageUrl": {
"type": "string",
"description": "the image URL to lock as the product (from a research result, a workspace / list_product_photos url, or any public product photo)"
},
"source_note": {
"type": "string",
"description": "a short note on where it came from, e.g. \"from their IG post\""
}
}
}Change a workspace member’s role — admin (full access incl. billing) or member (read-only on billing). A privilege change: confirm the exact person + new role with the user, then call with confirm:true.
| Argument | Type | Description |
|---|---|---|
| role* | string | the new role |
| email* | string | the member’s email |
| confirm | boolean | REQUIRED true |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"email",
"role"
],
"properties": {
"role": {
"enum": [
"admin",
"member"
],
"type": "string",
"description": "the new role"
},
"email": {
"type": "string",
"description": "the member’s email"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
}
}
}Create a SHARING LINK to a OneDrive file or folder (fileId from list_onedrive_files): type view (read-only, default), edit, or embed (an <iframe> link, personal OneDrive only); scope anonymous (anyone with the link, default), organization (work or school OneDrive only) or users (only people you invite); optional expiresAt (date or ISO) and password (on a personal OneDrive both need a Microsoft 365 subscription; the refusal says so). An ANONYMOUS EDIT link lets anyone holding it change or delete the file, so it needs confirm:true after you show the user that; every other link is created straight away. If Hermoso already made the same kind of link, OneDrive returns that one and the reply says it was reused. Read back from the item's permission list. 0 credits. Needs OneDrive connected.
| Argument | Type | Description |
|---|---|---|
| type | string | view (default), edit, or embed (personal OneDrive only) |
| scope | string | anonymous (default, anyone with the link), organization (work or school), or users (invited people only) |
| fileId* | string | OneDrive file or folder id from list_onedrive_files |
| confirm | boolean | required for an anonymous edit link |
| password | string | personal OneDrive with Microsoft 365 |
| expiresAt | string | e.g. 2026-11-30 or 2026-11-30T17:00:00Z |
Render a multi-scene STITCHED video (≥2 scenes) — ONLY for spots LONGER than ONE clip of the chosen model. A multi-beat ad that FITS one clip renders better and cheaper as ONE single-pass generate_video/render_ad (a single generation carries the whole hook->demo->payoff arc) — never stitch those. What fits is the model’s own maximum from hermoso_capabilities, not a fixed number: 15s on most models, 30s on the longest-clip one, so a 30s spot need not be stitched at all if you name that model. Blocks until done. Spends credits.
| Argument | Type | Description |
|---|---|---|
| model | string | video model id from hermoso_capabilities — omit to let the router pick |
| voice | string | voiceover voice name, e.g. Rachel / George |
| scenes* | array | array of scene objects (visual + optional voiceover/seconds) |
| voiceover | string | full voiceover script spoken across the scenes |
| resolution | string | 720p (default), 1080p for full detail, or 480p for a cheaper draft |
| aspectRatio | string | output aspect ratio, e.g. 9:16 (default) / 1:1 / 16:9 |
| durationSeconds | number | total spot length in seconds (defaults to the sum of the scenes’ seconds) |
Read one of this workspace’s data stores by key, for visibility into what the app holds — playbooks, swipefile, saved locations, avatars, creations, chats, brand, memory, skills. Read-only, free. Allowed keys: heist.memory.v1, heist.skills.v1, heist.playbooks.v1, heist.avatars.v1, heist.locations.v1, heist.chats.v1, heist.creations.v1, heist.assets.v1, heist.brand.v1, adInspo.swipefile.v1. (The typed tools — list_memory / list_skills / get_brand — are friendlier for those; use store_get for the rest.)
| Argument | Type | Description |
|---|---|---|
| key* | string | the store key to read (one of the allowlisted keys) |
| limit | number | max array items to return (default 50) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "the store key to read (one of the allowlisted keys)"
},
"limit": {
"type": "number",
"description": "max array items to return (default 50)"
}
}
}Clean up and consolidate the workspace Memory: drops entries that are about how Hermoso, a tool, a connector or a platform API behaves (product behaviour, not the brand), drops phone numbers and emails, and merges near-duplicate facts into one sentence each. Call with no argument to get the PROPOSAL (what would be removed and merged, with reasons) — nothing changes. Call again with confirm:true to apply it through the same typed writers the app uses (deletes carry tombstones so they stay deleted on every device). One model call, billed at its token cost.
| Argument | Type | Description |
|---|---|---|
| brand | string | profile id/name from list_brands, this call only |
| confirm | boolean | true to APPLY the proposal; omit to only see it |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"confirm": {
"type": "boolean",
"description": "true to APPLY the proposal; omit to only see it"
}
}
}What to post on TikTok, from posts that are working THIS MONTH in the brand's niche: up to 6 content ideas (hook, format, caption, why, an act-within window of 7d or 30d, a quality tier), EACH backed by linked TikTok posts with their views, likes and engagement. One bounded pull of the last 30 days: up to 4 keyword searches + up to 5 hashtag pulls, then up to 8 reads of what the strongest posts SAY in their first seconds; only posts past 50,000 views back an idea, and an idea the catalog cannot back is dropped. Works with nothing connected. PAID and priced first: dryRun:true returns the exact credits (lookups + writing) with nothing run; the run charges exactly that (the writing part is not charged if no post clears the bar). THE PICK: call again with pick (one idea object from `ideas`) and to ('render_ad' or 'make_series'), free: it returns the next call's arguments (plan_ad then render_ad, or make_series) with the CLAIMS the ad may make, taken only from the brand's own facts. Nothing renders until you make that call. Omit keywords/hashtags to derive them from product/category or the saved brand.
| Argument | Type | Description |
|---|---|---|
| to | string | where the pick goes: render_ad (plan_ad then render_ad, the default) or make_series |
| goal | string | what the posts should do: sales, followers, awareness… |
| pick | object | THE PICK: one idea object exactly as this tool returned it in `ideas` |
| reads | integer | how many of the strongest posts to read for their spoken hook, 0-8 (default 4) |
| claims | array | with pick: extra claims the USER stated about their product (added to the brand facts) |
Patch SPECIFIC fields of the active profile (name, domain, sells, summary, category, audience, positioning, voice, style, goal, logos, brand/product name pronunciations, profileType: explore = kept but unused) WITHOUT overwriting the rest — a read-modify-write on the saved brand. Use for “change our voice to playful”, “we sell to dentists now”. To onboard a brand from scratch, use draft_brand. If no brand is saved yet and you only INFERRED one from what the user is making, ask them to confirm it is their brand before saving it (they may be working for a client or just trying things). Only pass the fields you’re changing.
| Argument | Type | Description |
|---|---|---|
| goal | string | current marketing goal |
| logo | string | logo image: URL or local file path to a PNG, JPEG or WebP (no SVG) |
| name | string | brand name |
| brand | string | profile id/name from list_brands, this call only |
| sells | string | what the brand sells |
| style | string | visual style — palette, typography, aesthetic |
| voice | string | brand voice/tone |
| domain | string |
EDIT a Google Doc — the correction append_to_doc cannot make. Two shapes: `replacements:[{find, replace}]` rewrites specific text wherever it appears (call read_doc first and match the text EXACTLY; matchCase:false ignores case), or `rewrite:"…"` replaces the ENTIRE body (rewrite:"" empties it), or `dropdowns:[{title, value}]` sets a dropdown chip (e.g. Status -> Approved — read_doc lists every chip with its options; pass dropdownId when two share a title; an unknown title or option is refused with the real list and nothing changes); the same entry with options:[…] (the COMPLETE new list, 2–50) and/or newTitle changes the dropdown itself for every chip built from it, and an option you drop that a chip shows needs replace:{"Old":"New"}. Find/replace runs immediately and REPORTS how many occurrences changed — zero matches is reported as a FAILURE to match, never as a quiet success, because a text edit that silently does nothing is worse than one that visibly fails. A whole-body rewrite is destructive: call it without confirm first to get the character count, then confirm:true + confirmCells. Both are index-free by design — an agent cannot reliably compute Google’s character offsets, and a wrong offset deletes the wrong sentence.
| Argument | Type | Description |
|---|---|---|
| docUrl | string | a Google Docs URL — the id is extracted from it |
| confirm | boolean | true runs a whole-body rewrite after the character count is shown |
| rewrite | string | replace the WHOLE body with this text ("" empties the doc) |
| dropdowns | array | dropdown chips to set, or dropdowns to change |
Update a Drive file: rename (name), move it into a folder (moveToFolderId, optionally removeFromFolderId to move OUT of the old one), or trash / untrash it (trash:true|false). Pass fileId (from list_drive_files). To delete permanently, use delete_drive_file.
| Argument | Type | Description |
|---|---|---|
| name | string | new name |
| trash | boolean | true -> move to Trash; false -> restore from Trash |
| fileId* | string | the Drive file id |
| moveToFolderId | string | folder id to move the file into (from create_drive_folder / list_drive_files) |
| removeFromFolderId | string | the old parent folder id to remove (when moving) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"name": {
"type": "string",
"description": "new name"
},
"trash": {
"type": "boolean",
"description": "true -> move to Trash; false -> restore from Trash"
},
"fileId": {
"type": "string",
"description": "the Drive file id"
},
"moveToFolderId": {
"type": "string",
"description": "folder id to move the file into (from create_drive_folder / list_drive_files)"
},
"removeFromFolderId": {
"type": "string",
"description": "the old parent folder id to remove (when moving)"
}
}
}Change properties on ONE row of a Notion database (pageId from query_notion_database) — e.g. mark a calendar row Status "Posted" and put the live post link in its URL column. properties is {"Column": value} by column name: text, number, checkbox, select (an existing option or a new one), status (must be an existing option), multi-select (array; replaces the whole value), date (ISO, or {start, end}), url, email, phone; null clears. Validated against the database's own columns before anything is sent and refused by name otherwise, never coerced; columns you do not name keep their values. Read back from Notion. trash:true moves the row to Notion's trash (restorable; returns a preview first and needs confirm:true), trash:false restores it. Free.
| Argument | Type | Description |
|---|---|---|
| trash | boolean | true moves the row to Notion trash (needs confirm); false restores it |
| pageId* | string | the row id or link |
| confirm | boolean | true confirms moving the row to trash; not needed for other changes |
| properties | object | {"Column": value} |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"pageId"
],
"properties": {
"trash": {
"type": "boolean",
"description": "true moves the row to Notion trash (needs confirm); false restores it"
},
"pageId": {
"type": "string",
"description": "the row id or link"
},
"confirm": {
"type": "boolean",
"description": "true confirms moving the row to trash; not needed for other changes"
},
"properties": {
"type": "object",
"description": "{\"Column\": value}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}Update a OneDrive item: rename (name) and/or move it into a folder (moveToFolderId). Pass fileId (from list_onedrive_files). To remove an item, use delete_onedrive_file.
| Argument | Type | Description |
|---|---|---|
| name | string | new name |
| fileId* | string | the OneDrive item id |
| moveToFolderId | string | folder id to move the item into (from create_onedrive_folder / list_onedrive_files) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"name": {
"type": "string",
"description": "new name"
},
"fileId": {
"type": "string",
"description": "the OneDrive item id"
},
"moveToFolderId": {
"type": "string",
"description": "folder id to move the item into (from create_onedrive_folder / list_onedrive_files)"
}
}
}Edit a catalog product's title, description, selling points, audience or page link. What you set here is kept through later page re-reads. Free.
| Argument | Type | Description |
|---|---|---|
| id* | string | catalog product id, as add_product returned it |
| url | string | product page link |
| title | string | product name |
| brandId | string | a profile id/name from list_brands; omit for the active profile |
| audience | string | who this product is for |
| description | string | product description |
| sellingPoints | array | selling points; replaces the saved list |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "catalog product id, as add_product returned it"
},
"url": {
"type": "string",
"description": "product page link"
},
"title": {
"type": "string",
"description": "product name"
},
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands; omit for the active profile"
},
"audience": {
"type": "string",
"description": "who this product is for"
},
"description": {
"type": "string",
"description": "product description"
},
"sellingPoints": {
"type": "array",
"items": {
"type": "string"
},
"description": "selling points; replaces the saved list"
}
}
}Set the OUTREACH STATUS and/or a NOTE on a creator already saved in the swipefile (find_creators -> save_to_swipefile, or the heart on a creator card). Status is one of new | contacted | replied | booked | passed. The note is free text (deal terms, rate, what was sent). Reads back the updated row. Use list_swipefile to find the key. Free.
| Argument | Type | Description |
|---|---|---|
| key* | string | the saved row's key from list_swipefile, e.g. tiktok:handle |
| note | string | replaces the existing note; pass "" to clear it |
| brand | string | profile id/name from list_brands, this call only |
| status | string | outreach status: new, contacted, replied, booked or passed |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "the saved row's key from list_swipefile, e.g. tiktok:handle"
},
"note": {
"type": "string",
"maxLength": 2000,
"description": "replaces the existing note; pass \"\" to clear it"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"status": {
"enum": [
"new",
"contacted",
"replied",
"booked",
"passed"
],
"type": "string",
"description": "outreach status: new, contacted, replied, booked or passed"
}
}
}Change this account's app settings. language = the language EVERY ad, script and plan is written in from now on (chat replies follow the language the user writes in) (say the language in plain English, e.g. "German", "Japanese", "Brazilian Portuguese") — it applies to renders made over MCP as well as in the app. theme = the app's appearance, "dark" or "light". watchEmail = the weekly competitor-watch email on/off. Only pass what you are changing. Account-wide (every profile), and it takes effect on the next call.
| Argument | Type | Description |
|---|---|---|
| theme | string | app appearance |
| language | string | language for generated ads and copy (chat replies follow the language the user writes in) — e.g. "English", "German", "Japanese" |
| watchEmail | boolean | weekly competitor-watch email on/off |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"theme": {
"enum": [
"dark",
"light"
],
"type": "string",
"description": "app appearance"
},
"language": {
"type": "string",
"description": "language for generated ads and copy (chat replies follow the language the user writes in) — e.g. \"English\", \"German\", \"Japanese\""
},
"watchEmail": {
"type": "boolean",
"description": "weekly competitor-watch email on/off"
}
}
}CORRECT cells in a Google Sheet — write values to an exact range, overwriting whatever is there. This is the fix append_to_sheet cannot make: appending only ever adds rows at the bottom, so without this a wrong number stays wrong forever and the only "correction" is a second row contradicting the first. Pass `range` (e.g. "B2:C5", or "Q3 Report!B2" to name a tab — list_sheet_tabs gives the names) and `values` as an array of row arrays; an anchor cell like "B2" is fine and the block is written down and right from it. Writing into EMPTY cells goes straight through. Writing OVER cells that already hold values is REFUSED first, naming exactly how many filled cells would be overwritten — show the user that, get a yes, then call again with confirm:true. The result is READ BACK from the sheet, so what you report is what the sheet now holds rather than what Google accepted.
| Argument | Type | Description |
|---|---|---|
| range | string | A1 range or anchor cell, e.g. "B2:C5", "B2", or "Q3 Report!B2" (default A1) |
| values | array | array of row arrays to write |
| confirm | boolean | required only when the target range already holds values |
| updates | array | write SEVERAL disjoint ranges in one call, instead of range+values |
| sheetUrl | string | Google Sheets link, instead of spreadsheetId |
| spreadsheetId | string |
Change the SUBSCRIPTION plan (admin only). No argument lists the monthly plans. Basic ($9.99) is the posting plan: unlimited posting and Autopilot with NO monthly credits (it includes the same 250+ free credits as the Free plan, monthly credits start on Starter, and Autopilot spends credits). Starter, Pro and Studio include monthly credits and no markup on models. Every paid plan posts without a limit (Free: 10 posts a month on 3 channels). New subscriber: a Checkout URL for the human. Existing subscriber upgrading: a quote + quote_token; once the human agrees, call again with confirm:true and quote_token to charge the card on file. Downgrade: a billing-page link; it starts at the term's end.
| Argument | Type | Description |
|---|---|---|
| plan | string | the plan id to move to (e.g. pro) — omit to list the available plans first |
| period | string | monthly only ('yr' buys monthly) |
| confirm | boolean | true (with quote_token) to make a quoted upgrade |
| quote_token | string | the quoteToken from the quote step |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"plan": {
"type": "string",
"description": "the plan id to move to (e.g. pro) — omit to list the available plans first"
},
"period": {
"enum": [
"mo",
"yr"
],
"type": "string",
"description": "monthly only ('yr' buys monthly)"
},
"confirm": {
"type": "boolean",
"description": "true (with quote_token) to make a quoted upgrade"
},
"quote_token": {
"type": "string",
"description": "the quoteToken from the quote step"
}
}
}Persist an ARBITRARY user file (image, video, audio, PDF or document, up to 150MB) into Hermoso and get back a durable public URL that EVERY publish, schedule and ad-build tool accepts. Kept in the Library. THIS IS THE BRING-YOUR-OWN-CREATIVE PATH: it is for files that have NOTHING to do with a Hermoso render (media on the user's desktop, an agency's finished ad, a photo they shot), and it means you can publish, schedule and run ads through Hermoso without generating anything here. Provide exactly ONE source — passing two is an error, never a silent preference: `url` (ANY public http(s) link — Hermoso fetches it server-side, so nothing crosses this connection and there is no practical size limit; THIS IS THE ONE THAT ALWAYS WORKS, including on the hosted connector), `dataUri` (a base64 data: URI — keep it under ~15MB, since the bytes travel over this connection). If the file is already at a public https URL, the Meta, Reddit and ChatGPT-Ads tools take it directly and re-host it safely. When in doubt, use this: its URL works everywhere. (Calling the underlying HTTP route directly? POST /api/upload takes the file's RAW BYTES as the request body with its own content-type — NOT multipart/form-data — or ?url=<link> with no body.) Returns {url, kind, bytes}.
| Argument | Type | Description |
|---|---|---|
| url | string | a PUBLIC http(s) URL Hermoso fetches server-side (private/internal addresses are refused, and every redirect hop is re-checked). Works on every surface including the hosted connector, and the bytes never cross this connection — prefer this whenever the file is reachable on the web. |
| name | string | original file name — helps pick the right extension |
| dataUri | string | base64 data: URI of the file bytes (data:<mime>;base64,<…>) — bytes travel over this connection, so keep it small |
| getUploadUrl |
Upscale a video to higher resolution (2x) for final delivery. Paid render; returns the served URL. A 480p Seedance 2.5 draft: finish_draft (same take, native 1080p). Three engines: the default ('standard') is the safe precision upscaler; engine:'flux' is the FLUX 3 video upscaler (1080p/2K/4K) with an optional mode:'creative' detail-enhancement pass; engine:'detail' rebuilds fine detail such as hair, skin and fabric, the sharpest result for a hero shot and the most expensive. Pick 'flux' or 'detail' when the user asks for added detail rather than a faithful enlargement.
| Argument | Type | Description |
|---|---|---|
| mode | string | FLUX only — 'creative' turns on its detail-enhancement pass; default precise |
| video* | string | the source video URL; a URL, not a local file path; use upload_file for local files |
| engine | string | default 'standard', the precision upscaler. 'flux' = the FLUX 3 video upscaler. 'detail' = rebuilds fine detail, costs the most |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video"
],
"properties": {
"mode": {
"enum": [
"precise",
"creative"
],
"type": "string",
"description": "FLUX only — 'creative' turns on its detail-enhancement pass; default precise"
},
"video": {
"type": "string",
"description": "the source video URL; a URL, not a local file path; use upload_file for local files"
},
"engine": {
"enum": [
"standard",
"flux",
"detail"
],
"type": "string",
"description": "default 'standard', the precision upscaler. 'flux' = the FLUX 3 video upscaler. 'detail' = rebuilds fine detail, costs the most"
}
}
}Pin which profile this connection acts on. Pass the profile id or exact name from list_brands. Works for your own profiles AND one another account SHARED with you (pass its name or the profile id list_brands prints); access is verified against your real membership before it is pinned. Persists for this API key until changed, on every surface (hosted connector included — no environment variables, no restart).
| Argument | Type | Description |
|---|---|---|
| brand* | string | profile id (e.g. default / p_xxx), its exact name from list_brands, or the profile id of one shared with you |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"brand"
],
"properties": {
"brand": {
"type": "string",
"description": "profile id (e.g. default / p_xxx), its exact name from list_brands, or the profile id of one shared with you"
}
}
}LOOK at a video's exact frames, free: one contact-sheet image of the frames from start to end at fps (e.g. a seam, 3.0-3.8 s at 10 fps) or at the listed times, tiles in reading order with each tile's second listed. Use it to find a moment before an edit and to check a render's seam after it. Takes a Library / render URL, a direct file or a public post link (a TikTok / Reel / Facebook / X post link is opened through one paid lookup, 1 credit; a file is free). SAVE STILLS: save:true returns full-size still images (frame grabs, up to 8) as URLs instead of a contact sheet, picked by times or by clips (numbered at the video's hard cuts, -1 = the last): a before / after from someone's own footage, a cover, a product shot. Compose with those URLs (make_template_ad template 'custom', a post) with no AI.
| Argument | Type | Description |
|---|---|---|
| end | number | window end in seconds |
| fps | number | frames per second in the window (default ~16 frames across it) |
| url* | string | the video |
| save | boolean | true = save the frames at times / clips as full-size stills and return their URLs |
| clips | array | save: one still from the middle of each numbered clip (split at hard cuts; negative counts from the end) |
| start | number | window start in seconds |
| times | array | exact seconds instead of a window (max 48; max 8 with save) |
We read every tool's name, description and inputs the way the model will, looking for instructions hidden from you. A clean result means no warning signs, not a guarantee. Scanned Nov 6, 2026.
Takes a password, API key or token as an argument, so the secret passes through the model's context and chat logs.
Fix: Read credentials from the server's own configuration (environment, OAuth) instead of a tool argument.
A very long tool description (over 4,000 characters) is hard to review and easy to hide instructions in.
Fix: Keep the description to what the tool does and when to use it; move reference material to resources or docs.
| array |
| bullet-point selling points, one per item |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "the product page link"
},
"price": {
"type": "string",
"description": "hand-added only; never printed as current unless read from a page"
},
"title": {
"type": "string",
"description": "to add a product by hand (no link)"
},
"images": {
"type": "array",
"items": {
"type": "string"
},
"description": "photo urls for a hand-added product"
},
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands; omit for the active profile"
},
"audience": {
"type": "string",
"description": "who this product is for"
},
"currency": {
"type": "string",
"description": "currency code for the price, e.g. USD"
},
"description": {
"type": "string",
"description": "product description"
},
"sellingPoints": {
"type": "array",
"items": {
"type": "string"
},
"description": "bullet-point selling points, one per item"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"channel"
],
"properties": {
"limit": {
"type": "number",
"description": "how many posts this page (default 50, max 200)"
},
"cursor": {
"type": "string",
"description": "resume from a previous run"
},
"channel": {
"enum": [
"facebook",
"instagram",
"threads",
"youtube",
"tiktok",
"pinterest",
"bluesky"
],
"type": "string",
"description": "which channel to import from"
},
"confirm": {
"type": "boolean",
"description": "actually import — omit for a dry run that only quotes the cost"
},
"accountRef": {
"type": "string",
"description": "which Page / account, when the profile has more than one"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"pack": {
"type": "string",
"description": "the pack id to buy (e.g. pack-2k) — omit to list the available packs first"
},
"confirm": {
"type": "boolean",
"description": "set true to actually charge the saved card for `pack` (required for the one-click charge; ignored on the checkout-link path)"
},
"quote_token": {
"type": "string",
"description": "the quoteToken returned by the quote step — REQUIRED (with confirm:true) to charge; it binds the exact pack + price you quoted (10-minute validity) and makes a retried confirm idempotent"
}
}
}| captions | boolean | burn subtitles into every clip. DEFAULT TRUE — a clip cut from a podcast or a talk is watched on mute, and the words are the product. Set false for clean footage. A clip whose window carries no readable speech is delivered bare rather than captioned with a guess, and the result says which. |
| aspectRatio | string | clip shape: '9:16' (default), '1:1', '16:9', any 'W:H', or 'keep' for the source framing |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video"
],
"properties": {
"count": {
"type": "number",
"description": "how many clips to cut, 1-8 (default 4)"
},
"video": {
"type": "string",
"description": "the long video to clip — a YouTube/Vimeo/Loom/Dailymotion/Streamable/Rumble/Wistia/Twitch/TED watch URL, a direct https .mp4/.mov/.webm, or a Hermoso /generated/ URL; a URL, not a local file path; use upload_file for local files"
},
"captions": {
"type": "boolean",
"description": "burn subtitles into every clip. DEFAULT TRUE — a clip cut from a podcast or a talk is watched on mute, and the words are the product. Set false for clean footage. A clip whose window carries no readable speech is delivered bare rather than captioned with a guess, and the result says which."
},
"aspectRatio": {
"type": "string",
"description": "clip shape: '9:16' (default), '1:1', '16:9', any 'W:H', or 'keep' for the source framing"
}
}
}| durationSeconds | number | override the length in seconds; omit to match the original |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "the video to clone — a TikTok / Instagram Reel / Facebook / X / YouTube link, or a direct https video file URL; a URL, not a local file path; use upload_file for local files"
},
"brand": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {},
"additionalProperties": {}
}
],
"description": "brand name or profile object; OMIT to use the workspace's saved brand (see get_brand)"
},
"changes": {
"type": "string",
"description": "what to change or keep from the original, in the user's words (e.g. \"same hook but in a gym\", \"keep the jump cut, older creator\")"
},
"product": {
"type": "string",
"description": "what the new ad sells, plus any angle or offer; omit to use the saved brand's product"
},
"language": {
"type": "string",
"description": "language for the new ad's script and copy — default English"
},
"durationSeconds": {
"type": "number",
"description": "override the length in seconds; omit to match the original"
}
}
}| string |
| the connector id: applovin_ads, stripe, intercom, mailchimp, klaviyo, vercel, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, wordpress, slack, discord, webhook |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider"
],
"properties": {
"fields": {
"type": "object",
"description": "that provider's own field names and values, e.g. {\"apiKey\":\"…\"}; the names for each provider are in the description",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"provider": {
"type": "string",
"description": "the connector id: applovin_ads, stripe, intercom, mailchimp, klaviyo, vercel, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, wordpress, slack, discord, webhook"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"brand"
],
"properties": {
"brand": {
"type": "string",
"description": "profile id or exact name from list_brands"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — this destroys the whole profile and cannot be undone"
},
"confirmName": {
"type": "string",
"description": "the workspace's EXACT name, required when it is not empty — copy it from the inventory this tool returned, after the user has agreed to it"
},
"confirmConnectors": {
"type": "number",
"description": "the number of connected accounts the inventory reported, required when there is at least one — the user must specifically agree to losing them, because reconnecting each needs a browser and no agent can do it"
}
}
}| converting |
| boolean |
| pass false ONLY when the user has told you these posts are getting seen and are not converting — it re-reads the ones that are earning their reach as an offer problem instead of a win. Omit when you do not know; we cannot measure it. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "how many recent posts to diagnose (default 25, max 200). The baseline is always built from EVERY post recorded for the brand, never only these, so a bad month can never become its own definition of normal."
},
"channel": {
"type": "string",
"description": "restrict to one channel: facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky"
},
"converting": {
"type": "boolean",
"description": "pass false ONLY when the user has told you these posts are getting seen and are not converting — it re-reads the ones that are earning their reach as an offer problem instead of a win. Omit when you do not know; we cannot measure it."
}
}
}| a social handle (creators); needs platform, never guessed |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"save": {
"type": "boolean",
"description": "save onto the active profile (like Studio onboarding) so plan_ad/create use it automatically. Default: saves only when NO brand is saved yet; pass true to REPLACE the saved profile (the drafted fields overwrite the saved ones), false to never save"
},
"domain": {
"type": "string",
"description": "a website to scrape, or a social link (profile, YouTube channel or one post) to draft from the account it names"
},
"platform": {
"type": "string",
"description": "socialHandle platform: instagram/tiktok/youtube/twitter/threads"
},
"confirmed": {
"type": "boolean",
"description": "user confirmed it is their brand: read + save site product photos (paid)"
},
"description": {
"type": "string",
"description": "a free-text brand description (no website)"
},
"socialHandle": {
"type": "string",
"description": "a social handle (creators); needs platform, never guessed"
}
}
}| dryRun | boolean | true = return what this edit usually costs in credits and render nothing |
| markup | string | MARKUP-GUIDED EDIT: drawn annotations (circles, arrows, scribbles) showing what to change, as a URL or local path: a transparent PNG of just the marks, or a copy of the same image with the marks drawn on it. Say the change in `instruction` ("make the circled vase blue"); the marks are not in the result. Runs on gpt-image-2.6 (or the editor named in `model`). Never with a mask |
| removal | boolean | true when the edit REMOVES text, branding, a logo, a watermark, a person or an object, so the brand name and logo are not re-added |
| fixLabel | boolean | false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product) |
| complexity | string | gpt-image-2.6 editors only: how much it plans before drawing (default high; low suits a small focused change) |
| instruction* | string | the change to make, in plain words (pass the user’s own words for a removal or plain photo edit) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"image",
"instruction"
],
"properties": {
"logo": {
"type": "boolean"
},
"mask": {
"type": "string",
"description": "optional mask image (URL or local path) marking the region to change: transparent = change, or white = change on an opaque mask"
},
"image": {
"type": "string",
"description": "the image to edit: URL, Library item URL, upload_file URL or local path"
},
"model": {
"type": "string",
"description": "the editor, only when the user names one: gpt-image-2.6, gpt-image-2.6-flare (its own faster editor) or gpt-image-2.5. Omit for the default"
},
"dryRun": {
"type": "boolean",
"description": "true = return what this edit usually costs in credits and render nothing"
},
"markup": {
"type": "string",
"description": "MARKUP-GUIDED EDIT: drawn annotations (circles, arrows, scribbles) showing what to change, as a URL or local path: a transparent PNG of just the marks, or a copy of the same image with the marks drawn on it. Say the change in `instruction` (\"make the circled vase blue\"); the marks are not in the result. Runs on gpt-image-2.6 (or the editor named in `model`). Never with a mask"
},
"removal": {
"type": "boolean",
"description": "true when the edit REMOVES text, branding, a logo, a watermark, a person or an object, so the brand name and logo are not re-added"
},
"fixLabel": {
"type": "boolean",
"description": "false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)"
},
"complexity": {
"enum": [
"low",
"medium",
"high"
],
"type": "string",
"description": "gpt-image-2.6 editors only: how much it plans before drawing (default high; low suits a small focused change)"
},
"instruction": {
"type": "string",
"description": "the change to make, in plain words (pass the user’s own words for a removal or plain photo edit)"
}
}
}| Argument | Type | Description |
|---|---|---|
| fps | number | the frame rate, a whole number from 24 to 60 (24, 25, 30, 60 are the usual ones). Leave it out: the canvas takes the rate of its own footage (the rate of most of it on screen), so no frames are invented; a faster clip is thinned, a slower one that does not divide it is motion-interpolated |
| sfx | array | sound effects placed on the output timeline |
| size | string | canvas: '9:16' default, '1:1', '4:5', '16:9', any 'W:H' (1:3 to 3:1, on a 1080 short side) or exact 'WxH' pixels |
| match | any | every cut: 'auto' default |
| review | boolean | default true: a vision read of each seam (about 2 credits) comes back with the render, with the seam frames |
| reframe | any | every cut: 'auto' default |
| overlays | array | HTML/CSS overlays animated with @keyframes, each shown between its start and end |
| segments* | array | the clips on the output, max 12, later ones on top. Every animated field (scale x y rotate opacity exposure blur mblur brightness contrast saturation, audio.gain) is a constant or keyframes [{t: seconds into this segment | src: a second of the source, v, ease: linear|in|out|inout|hold}]; x and y are in canvas widths / heights, rotate in degrees, mblur in px along mblurAngle |
| videoUrl | string | the video 'this' refers to (optional when every segment names its own src) |
| motionBlur | boolean | default true: anything that moves gets a real shutter blur along its path |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"segments"
],
"properties": {
"fps": {
"type": "number",
"description": "the frame rate, a whole number from 24 to 60 (24, 25, 30, 60 are the usual ones). Leave it out: the canvas takes the rate of its own footage (the rate of most of it on screen), so no frames are invented; a faster clip is thinned, a slower one that does not divide it is motion-interpolated"
},
"sfx": {
"type": "array",
"items": {
"type": "object",
"required": [
"sound",
"at"
],
"properties": {
"at": {
"type": "number",
"description": "output second the sound lands"
},
"gain": {
"type": "number",
"description": "level in dB, -30 to 6; omit to match the sound under it"
},
"sound": {
"enum": [
"whoosh",
"impact",
"payoff"
],
"type": "string",
"description": "whoosh, impact or payoff"
},
"segment": {
"type": "number",
"description": "payoff: the segment whose OWN payoff sound (the egg's real splat) lands at `at`"
}
}
},
"description": "sound effects placed on the output timeline"
},
"size": {
"type": "string",
"description": "canvas: '9:16' default, '1:1', '4:5', '16:9', any 'W:H' (1:3 to 3:1, on a 1080 short side) or exact 'WxH' pixels"
},
"match": {
"anyOf": [
{
"enum": [
"auto",
"off"
],
"type": "string"
},
{
"type": "object",
"properties": {
"blur": {
"type": "boolean"
},
"grade": {
"type": "boolean"
},
"grain": {
"type": "boolean"
},
"level": {
"type": "boolean"
},
"strength": {
"type": "number"
}
}
}
],
"description": "every cut: 'auto' default"
},
"review": {
"type": "boolean",
"description": "default true: a vision read of each seam (about 2 credits) comes back with the render, with the seam frames"
},
"reframe": {
"anyOf": [
{
"enum": [
"auto",
"off"
],
"type": "string"
},
{
"type": "number"
}
],
"description": "every cut: 'auto' default"
},
"overlays": {
"type": "array",
"items": {
"type": "object",
"required": [
"html",
"start",
"end"
],
"properties": {
"end": {
"type": "number",
"description": "output second the overlay disappears"
},
"html": {
"type": "string",
"description": "HTML/CSS animated with @keyframes; no script, no network (images as data: URIs)"
},
"mode": {
"enum": [
"over",
"mask"
],
"type": "string",
"description": "'mask': its opaque pixels are where `segment` shows"
},
"start": {
"type": "number",
"description": "output second the overlay appears"
},
"segment": {
"type": "number",
"description": "for mode mask: 0-based index of the segment whose opaque shape it reveals"
}
}
},
"description": "HTML/CSS overlays animated with @keyframes, each shown between its start and end"
},
"segments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"x": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "canvas widths"
},
"y": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "canvas heights"
},
"at": {
"type": "number",
"description": "output second it starts (default: right after the previous)"
},
"in": {
"type": "number",
"description": "source second to start from (default 0)"
},
"fit": {
"enum": [
"auto",
"fill",
"contain",
"blur"
],
"type": "string",
"description": "how the picture fills the canvas: auto, fill, contain or blur"
},
"out": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"const": "payoff"
}
],
"description": "source second to stop at, or 'payoff' (default: the end)"
},
"src": {
"type": "string",
"description": "'this' or a video URL / public post link"
},
"blur": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "constant or keyframes [{t or src, v, ease}]"
},
"crop": {
"anyOf": [
{
"type": "string",
"const": "auto"
},
{
"type": "object",
"properties": {
"h": {
"type": "number"
},
"w": {
"type": "number"
},
"x": {
"type": "number"
},
"y": {
"type": "number"
}
}
}
],
"description": "fractions of the source frame, or 'auto': the picture only, without letterbox bars and their caption"
},
"hold": {
"type": "array",
"items": {
"type": "object",
"required": [
"src",
"seconds"
],
"properties": {
"src": {
"type": "number",
"description": "source second of the frame to freeze"
},
"seconds": {
"type": "number",
"description": "how long to hold that frame, in seconds"
}
}
},
"description": "freeze frames"
},
"audio": {
"type": "object",
"properties": {
"gain": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "dB"
},
"lead": {
"type": "number",
"description": "seconds its sound starts before its picture (a J-cut)"
},
"mute": {
"type": "boolean",
"description": "true silences this segment's sound"
},
"tail": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"const": "payoff"
}
],
"description": "seconds its sound runs past the cut (an L-cut), or payoff for the hook's own payoff sound"
},
"level": {
"enum": [
"match",
"keep"
],
"type": "string",
"description": "match levels its loudness to the clips around it; keep leaves it as recorded"
},
"fadeIn": {
"type": "number",
"description": "fade-in length in seconds for this segment's sound"
},
"fadeOut": {
"type": "number",
"description": "fade-out length in seconds for this segment's sound"
}
},
"description": "this segment's sound: mute, gain, lead, tail, fades and level"
},
"image": {
"type": "string",
"description": "a still instead (URL), with seconds"
},
"match": {
"anyOf": [
{
"enum": [
"auto",
"off"
],
"type": "string"
},
{
"type": "object",
"properties": {
"blur": {
"type": "boolean"
},
"grade": {
"type": "boolean"
},
"grain": {
"type": "boolean"
},
"level": {
"type": "boolean"
},
"strength": {
"type": "number"
}
}
}
],
"description": "the cut INTO this segment (default: the timeline match)"
},
"mblur": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "directional motion blur px"
},
"scale": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "constant or keyframes [{t or src, v, ease}]"
},
"speed": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {
"type": "object",
"required": [
"src",
"v"
],
"properties": {
"v": {
"type": "number"
},
"src": {
"type": "number"
},
"ease": {
"enum": [
"linear",
"hold"
],
"type": "string"
}
}
}
}
],
"description": "0.05-8, or a ramp over source seconds"
},
"anchor": {
"type": "object",
"required": [
"x",
"y"
],
"properties": {
"x": {
"type": "number",
"description": "canvas x, 0 to 1, that stays put while the segment scales"
},
"y": {
"type": "number",
"description": "canvas y, 0 to 1, that stays put while the segment scales"
}
},
"description": "canvas point (0-1) that stays put while it scales"
},
"radius": {
"type": "number",
"description": "rounded corners, a fraction of the picture's shorter side (0-0.5): a picture-in-picture CARD over the segment under it, any length"
},
"rotate": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "degrees"
},
"slowmo": {
"enum": [
"blend",
"hold",
"flow"
],
"type": "string",
"description": "how slow motion fills frames (flow = motion-interpolated)"
},
"between": {
"type": "boolean",
"description": "a bridge clip between its neighbours (a re-used generated shot): trimmed and graded to them automatically"
},
"opacity": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "constant or keyframes [{t or src, v, ease}]"
},
"overlap": {
"type": "number",
"description": "seconds it starts before the previous ends: the transition window"
},
"reframe": {
"anyOf": [
{
"enum": [
"auto",
"off"
],
"type": "string"
},
{
"type": "number"
}
],
"description": "the cut INTO this segment"
},
"reverse": {
"type": "boolean",
"description": "true plays the segment backwards at one constant speed, with no holds"
},
"seconds": {
"type": "number",
"description": "image segments: how long"
},
"contrast": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "constant or keyframes [{t or src, v, ease}]"
},
"exposure": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "× the light, 1 = as shot (a fine grade)"
},
"generate": {
"type": "object",
"required": [
"prompt"
],
"properties": {
"model": {
"type": "string",
"description": "a model id that takes start AND end frames"
},
"prompt": {
"type": "string",
"description": "description of a generated transition shot (paid; only when the user asks)"
},
"seconds": {
"type": "number",
"description": "3-8"
}
},
"description": "PAID: a new shot between its neighbours"
},
"brightness": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "-1..1, a flash"
},
"mblurAngle": {
"type": "number",
"description": "motion blur direction in degrees, used with mblur"
},
"saturation": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {}
}
],
"description": "constant or keyframes [{t or src, v, ease}]"
}
}
},
"description": "the clips on the output, max 12, later ones on top. Every animated field (scale x y rotate opacity exposure blur mblur brightness contrast saturation, audio.gain) is a constant or keyframes [{t: seconds into this segment | src: a second of the source, v, ease: linear|in|out|inout|hold}]; x and y are in canvas widths / heights, rotate in degrees, mblur in px along mblurAngle"
},
"videoUrl": {
"type": "string",
"description": "the video 'this' refers to (optional when every segment names its own src)"
},
"motionBlur": {
"type": "boolean",
"description": "default true: anything that moves gets a real shutter blur along its path"
}
}
}| lighting | string | default auto: keep the source light unless the change needs new light (night, a lamp, fire). 'preserve' or 'relight' forces it |
| faceRoute | string | 'face_lane' = the user's "I own the rights to this face" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own. |
| keepAudio | boolean | default true: keep the source audio; false = silent |
| previewAt | number | second to preview (default 0); resend with previewStill |
| reference | string | OPTIONAL image URL that anchors the MATERIAL of what changes (a fabric, a finish, a colour swatch, the real product). Only its surface is used, never its framing or light |
| instruction* | string | the change, in the user’s own words |
| textChanges | array | EVERY change of on-screen words at once ([{from, to}], e.g. each line and its translation). A video laid out from a design is made again with all of them in ONE render, about 2 credits, never a video edit |
| previewStill | string | the still a previewFirstFrame call returned; the clip then matches the changed thing to it |
| interactionId | string | OPTIONAL: the interactionId an earlier Gemini Omni render or edit returned; the edit continues that clip on the same Omni model. If it cannot run, the video editor edits it and the reply says so. |
| previewFirstFrame | boolean | true = edit ONE still of the first frame first and quote the clip; the paid clip does not run |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video",
"instruction"
],
"properties": {
"video": {
"type": "string",
"description": "the source video URL (a render, job result or list_library); a URL, not a local file path; use upload_file for local files"
},
"engine": {
"enum": [
"auto",
"seedance",
"kling"
],
"type": "string",
"description": "default auto: Seedance 2.5 Edit without a real-looking person, else Kling 4.0 Edit"
},
"literal": {
"type": "boolean",
"description": "true = send the instruction exactly as written, with no preserve/lock wrapper"
},
"elements": {
"type": "array",
"items": {
"type": "object",
"required": [
"frontal"
],
"properties": {
"refs": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 2 extra angles of the SAME subject"
},
"frontal": {
"type": "string",
"description": "the reference image URL"
}
},
"additionalProperties": {}
},
"description": "OPTIONAL identity/product grounding (≤4): a creator portrait or the real product photo, so the edit restores the REAL thing. Describe each one in the instruction"
},
"lighting": {
"enum": [
"auto",
"preserve",
"relight"
],
"type": "string",
"description": "default auto: keep the source light unless the change needs new light (night, a lamp, fire). 'preserve' or 'relight' forces it"
},
"faceRoute": {
"enum": [
"face_lane"
],
"type": "string",
"description": "'face_lane' = the user's \"I own the rights to this face\" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own."
},
"keepAudio": {
"type": "boolean",
"description": "default true: keep the source audio; false = silent"
},
"previewAt": {
"type": "number",
"description": "second to preview (default 0); resend with previewStill"
},
"reference": {
"type": "string",
"description": "OPTIONAL image URL that anchors the MATERIAL of what changes (a fabric, a finish, a colour swatch, the real product). Only its surface is used, never its framing or light"
},
"instruction": {
"type": "string",
"description": "the change, in the user’s own words"
},
"textChanges": {
"type": "array",
"items": {
"type": "object",
"required": [
"from",
"to"
],
"properties": {
"to": {
"type": "string"
},
"from": {
"type": "string"
}
}
},
"description": "EVERY change of on-screen words at once ([{from, to}], e.g. each line and its translation). A video laid out from a design is made again with all of them in ONE render, about 2 credits, never a video edit"
},
"previewStill": {
"type": "string",
"description": "the still a previewFirstFrame call returned; the clip then matches the changed thing to it"
},
"interactionId": {
"type": "string",
"description": "OPTIONAL: the interactionId an earlier Gemini Omni render or edit returned; the edit continues that clip on the same Omni model. If it cannot run, the video editor edits it and the reply says so."
},
"previewFirstFrame": {
"type": "boolean",
"description": "true = edit ONE still of the first frame first and quote the clip; the paid clip does not run"
}
}
}| also search Instagram’s creator marketplace (Meta’s own creator directory, free) for the same niche; needs the Meta connector |
| minAvgViews | number | minimum average views per post; creators below this floor are dropped |
| minEngagement | number | interactions per view, 0–1 (0.05 = 5%) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"niche"
],
"properties": {
"limit": {
"type": "number",
"description": "creators to return, 1–30 (default 12)"
},
"niche": {
"type": "string",
"description": "product category, topic or hashtag — \"calorie tracker app\", \"matcha\", \"#cleanbeauty\""
},
"enrich": {
"type": "boolean",
"description": "followers + public contact for the top 3 on TikTok and on Instagram (default true, 1 credit each)"
},
"queries": {
"type": "number",
"description": "query variants per platform, 1–4 (default 3); each is a paid search call"
},
"platforms": {
"type": "array",
"items": {
"enum": [
"tiktok",
"instagram",
"youtube"
],
"type": "string"
},
"description": "default all three"
},
"marketplace": {
"type": "boolean",
"description": "also search Instagram’s creator marketplace (Meta’s own creator directory, free) for the same niche; needs the Meta connector"
},
"minAvgViews": {
"type": "number",
"description": "minimum average views per post; creators below this floor are dropped"
},
"minEngagement": {
"type": "number",
"description": "interactions per view, 0–1 (0.05 = 5%)"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"videoUrl"
],
"properties": {
"sub": {
"type": "string",
"description": "accent sub-pill copy, ≤34 chars (usually the product/brand)"
},
"grain": {
"type": "boolean",
"description": "default false — anti-AI film-grain finish"
},
"pills": {
"type": "boolean",
"description": "default true — set false for a grain-only pass"
},
"accent": {
"type": "string",
"description": "brand accent hex for the sub-pill"
},
"header": {
"type": "string",
"description": "header pill copy, ≤40 chars (required when pills is on)"
},
"points": {
"type": "array",
"items": {
"type": "string"
},
"description": "3-4 proof points, ≤44 chars each"
},
"videoUrl": {
"type": "string",
"description": "the served URL of the video to finish (from a previous render/job); a URL, not a local file path; use upload_file for local files"
}
}
}| boolean |
| true = return the estimated credits this fix reserves and render nothing |
| prompt | string | what the replacement footage should show — describe the shot, matching the master's style. With `job`: the change to that block's picture, in words |
| refImage | string | optional product/style anchor image URL |
| videoUrl | string | the served URL of the master video to fix (not needed with `job`) |
| atSeconds | number | with `job`: the moment in the film instead of `block` ("the line at 0:12" = 12) |
| endSeconds | number | window end in seconds (window 1.5-8s; not needed with `job`) |
| startSeconds | number | window start in seconds (not needed with `job`) |
| speechWindows | array | [[start,end],...] windows with spoken lines — the fix window must not overlap these |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"job": {
"type": "string",
"description": "a finished video-blocks film (explainer / faceless channel): its job id. Edits one block of it instead of splicing a window"
},
"line": {
"type": "string",
"description": "with `job`: the new narration for that block, the exact words to say (said as written, never rewritten; refused for free when it cannot fit the block)"
},
"block": {
"type": "number",
"description": "with `job`: which block, 1-based (block 3 = 20-30s of a film of 10s blocks)"
},
"dryRun": {
"type": "boolean",
"description": "true = return the estimated credits this fix reserves and render nothing"
},
"prompt": {
"type": "string",
"description": "what the replacement footage should show — describe the shot, matching the master's style. With `job`: the change to that block's picture, in words"
},
"refImage": {
"type": "string",
"description": "optional product/style anchor image URL"
},
"videoUrl": {
"type": "string",
"description": "the served URL of the master video to fix (not needed with `job`)"
},
"atSeconds": {
"type": "number",
"description": "with `job`: the moment in the film instead of `block` (\"the line at 0:12\" = 12)"
},
"endSeconds": {
"type": "number",
"description": "window end in seconds (window 1.5-8s; not needed with `job`)"
},
"startSeconds": {
"type": "number",
"description": "window start in seconds (not needed with `job`)"
},
"speechWindows": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "number"
}
},
"description": "[[start,end],...] windows with spoken lines — the fix window must not overlap these"
}
}
}| string |
| motion direction, only for an engine other than standard that hermoso_capabilities lists |
| script* | string | the words the avatar speaks |
| resolution | string | '720p' (default) or '480p' |
| acceptQueue | boolean | only for an engine hermoso_capabilities marks oneAtATime: wait in line |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"image",
"script"
],
"properties": {
"seed": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "fixed seed, only for an engine other than standard"
},
"image": {
"type": "string",
"description": "local path or URL of the presenter portrait. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
},
"voice": {
"type": "string",
"description": "voice name (Aria / Sarah / George / Adam). Leave it out and the voice matches the person in `image`; if nobody can be read, the call is refused free asking for one"
},
"dryRun": {
"type": "boolean",
"description": "return the credits this exact job would hold, without rendering"
},
"engine": {
"type": "string",
"description": "leave out for the standard engine; only an engine listed in hermoso_capabilities avatarEngines is accepted"
},
"prompt": {
"type": "string",
"description": "motion direction, only for an engine other than standard that hermoso_capabilities lists"
},
"script": {
"type": "string",
"description": "the words the avatar speaks"
},
"resolution": {
"type": "string",
"description": "'720p' (default) or '480p'"
},
"acceptQueue": {
"type": "boolean",
"description": "only for an engine hermoso_capabilities marks oneAtATime: wait in line"
}
}
}| raw | boolean | RAW MODEL ACCESS: run the caller’s prompt on the named model with no Hermoso adjustments at all — the prompt reaches the provider byte-identical (no hex-to-colour-name rewrite, no prepended fidelity preamble) and NO saved-brand product photos are attached, so the model you name is the model that renders. Use it to drive the raw catalog; leave it off for an on-brand ad. Billing, the durable Library landing and per-model validation are unchanged. |
| mask | string | MASKED EDIT — change ONE region and keep the rest: a mask (path or URL, any size) for refImages[0]. TRANSPARENT = change, or on a mask with no transparency WHITE = change, black = keep; the reply says which it read. The mask guides rather than stencils, so the change can blend a little past its edge. Runs on gpt-image-2.6 (`refs.mask` in hermoso_capabilities): leave `model` empty or name it; another named model is refused, free. Needs refImages; keeps the source image's frame, so aspectRatio is not applied. |
| model | string | image model id from hermoso_capabilities. A model whose `refs.mode` is "edit" there (gpt-image-2.6, gpt-image-2.5) takes your refImages on ITS OWN editor, up to its `refs.max`, instead of the default compositor |
| format | string | the file to make (default png). gif = an ANIMATED GIF, a short loop with no sound (describe the subject, the motion and the loop); svg = an editable VECTOR (logo, icon, flat illustration). Both run on gpt-image-2.6 (default) or gpt-image-2.6-flare and take no refImages, mask or markup; the saved brand's photos stay off them. hermoso_capabilities prints each one's credits under `formats` |
| frames | integer | format gif only: frames in the loop, 2 to 16 (default 9) |
| prompt | string | REQUIRED on every model EXCEPT the pose rows below. the full image prompt — subject, composition, lighting, and any on-image ad text. ON A POSE MODEL (product-in-hand / try-on) THIS IS EXTRA DIRECTION AND IT IS OPTIONAL — leave it out and the pose is built for you. If you do write one, DESCRIBE THE POSE ("she holds the bottle upright in her right hand at chest height, label to camera"); do NOT phrase it as a swap ("replace the mug with the bottle"), which is REFUSED for free, because the product then comes out the size of whatever it replaced — a 30ml bottle rendered mug-sized in testing. |
| frameMs | integer | format gif only: milliseconds per frame, 20 to 2000, rounded to 10 (default 200; 9 frames x 200 = a 1.8 s loop) |
| fixLabel | boolean | default true: when the saved brand's product photo rides in this render, the product's label on the finished image is READ and compared with the photo, and re-printed from the photo at close range ONLY if it came out wrong (a label that is already right costs only the check; a re-print costs more, and dryRun:true returns the up-to price). The reply says whether the label was checked, fixed or left as rendered (`labelPass`). Pass false when the user wants the packaging left exactly as generated: nothing is checked or re-printed. |
| useBrand | boolean | default true: with no refImages, the server hydrates the SAVED brand’s product/logo references so the output lands on-brand; pass false for a pure prompt-only render |
| imageSize | string | pixel-size preset for models that support it: 1K/2K, and 4K on the models hermoso_capabilities lists with a 4K imageSize price (a 4K ask on any other model is refused, free) — omit for the default |
| refImages | array | local file paths or URLs of product/logo references to composite in. ON THE POSE MODELS — any row hermoso_capabilities marks `needsRefs` with a `refsMax`, such as putting your product in someone’s hands or a virtual try-on — THE ORDER IS THE CONTRACT AND IT IS NOT A COMPOSITE: refImages[0] is the PERSON photo, and the rest (up to `refsMax` minus one) are the product or garment photos. Reversed, you get the product wearing the person. A 4th product is dropped and the reply says so. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded. |
| complexity | string | gpt-image-2.6 / 2.6-flare only: how much the model plans before drawing (default high, best for dense text and complex layouts; low suits a small focused change). No separate charge |
| aspectRatio | string | e.g. '1:1', '9:16', '16:9', '4:5'. Each model draws its own list (hermoso_capabilities prints it per model, e.g. Nano Banana 2 goes to 1:8 and 8:1); a ratio the chosen model cannot draw is refused before anything is charged |
| logoPlacement | string | overlay: real logo file laid flat. in_scene: the scene leaves a blank patch on the object and the real file is printed onto it (perspective, light). Never drawn by the model. auto (default): from the prompt |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"job": {
"enum": [
"static_ad",
"product_photo",
"marketplace",
"thumbnail",
"character_sheet",
"vector"
],
"type": "string",
"description": "what the picture is for. With no `model` named it renders on the engine judged best for that job (hermoso_capabilities `jobEngines.image` lists them): static_ad (the default when neither `model` nor `job` is given) = a finished image ad with its headline, copy and button on the picture; product_photo = a studio, lifestyle, flat-lay or spotlight photo of the real product; marketplace = a listing card; thumbnail = a video cover (make_thumbnail is the full thumbnail pipeline); character_sheet = a multi-view sheet of one person; vector = a logo, icon or flat illustration as an editable SVG (it takes no reference pictures, so the saved brand's photos stay off it). A named `model` always wins"
},
"raw": {
"type": "boolean",
"description": "RAW MODEL ACCESS: run the caller’s prompt on the named model with no Hermoso adjustments at all — the prompt reaches the provider byte-identical (no hex-to-colour-name rewrite, no prepended fidelity preamble) and NO saved-brand product photos are attached, so the model you name is the model that renders. Use it to drive the raw catalog; leave it off for an on-brand ad. Billing, the durable Library landing and per-model validation are unchanged."
},
"mask": {
"type": "string",
"description": "MASKED EDIT — change ONE region and keep the rest: a mask (path or URL, any size) for refImages[0]. TRANSPARENT = change, or on a mask with no transparency WHITE = change, black = keep; the reply says which it read. The mask guides rather than stencils, so the change can blend a little past its edge. Runs on gpt-image-2.6 (`refs.mask` in hermoso_capabilities): leave `model` empty or name it; another named model is refused, free. Needs refImages; keeps the source image's frame, so aspectRatio is not applied."
},
"model": {
"type": "string",
"description": "image model id from hermoso_capabilities. A model whose `refs.mode` is \"edit\" there (gpt-image-2.6, gpt-image-2.5) takes your refImages on ITS OWN editor, up to its `refs.max`, instead of the default compositor"
},
"format": {
"enum": [
"png",
"gif",
"svg"
],
"type": "string",
"description": "the file to make (default png). gif = an ANIMATED GIF, a short loop with no sound (describe the subject, the motion and the loop); svg = an editable VECTOR (logo, icon, flat illustration). Both run on gpt-image-2.6 (default) or gpt-image-2.6-flare and take no refImages, mask or markup; the saved brand's photos stay off them. hermoso_capabilities prints each one's credits under `formats`"
},
"frames": {
"type": "integer",
"maximum": 16,
"minimum": 2,
"description": "format gif only: frames in the loop, 2 to 16 (default 9)"
},
"prompt": {
"type": "string",
"description": "REQUIRED on every model EXCEPT the pose rows below. the full image prompt — subject, composition, lighting, and any on-image ad text. ON A POSE MODEL (product-in-hand / try-on) THIS IS EXTRA DIRECTION AND IT IS OPTIONAL — leave it out and the pose is built for you. If you do write one, DESCRIBE THE POSE (\"she holds the bottle upright in her right hand at chest height, label to camera\"); do NOT phrase it as a swap (\"replace the mug with the bottle\"), which is REFUSED for free, because the product then comes out the size of whatever it replaced — a 30ml bottle rendered mug-sized in testing."
},
"frameMs": {
"type": "integer",
"maximum": 2000,
"minimum": 20,
"description": "format gif only: milliseconds per frame, 20 to 2000, rounded to 10 (default 200; 9 frames x 200 = a 1.8 s loop)"
},
"fixLabel": {
"type": "boolean",
"description": "default true: when the saved brand's product photo rides in this render, the product's label on the finished image is READ and compared with the photo, and re-printed from the photo at close range ONLY if it came out wrong (a label that is already right costs only the check; a re-print costs more, and dryRun:true returns the up-to price). The reply says whether the label was checked, fixed or left as rendered (`labelPass`). Pass false when the user wants the packaging left exactly as generated: nothing is checked or re-printed."
},
"useBrand": {
"type": "boolean",
"description": "default true: with no refImages, the server hydrates the SAVED brand’s product/logo references so the output lands on-brand; pass false for a pure prompt-only render"
},
"imageSize": {
"type": "string",
"description": "pixel-size preset for models that support it: 1K/2K, and 4K on the models hermoso_capabilities lists with a 4K imageSize price (a 4K ask on any other model is refused, free) — omit for the default"
},
"refImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "local file paths or URLs of product/logo references to composite in. ON THE POSE MODELS — any row hermoso_capabilities marks `needsRefs` with a `refsMax`, such as putting your product in someone’s hands or a virtual try-on — THE ORDER IS THE CONTRACT AND IT IS NOT A COMPOSITE: refImages[0] is the PERSON photo, and the rest (up to `refsMax` minus one) are the product or garment photos. Reversed, you get the product wearing the person. A 4th product is dropped and the reply says so. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
},
"complexity": {
"enum": [
"low",
"medium",
"high"
],
"type": "string",
"description": "gpt-image-2.6 / 2.6-flare only: how much the model plans before drawing (default high, best for dense text and complex layouts; low suits a small focused change). No separate charge"
},
"aspectRatio": {
"type": "string",
"description": "e.g. '1:1', '9:16', '16:9', '4:5'. Each model draws its own list (hermoso_capabilities prints it per model, e.g. Nano Banana 2 goes to 1:8 and 8:1); a ratio the chosen model cannot draw is refused before anything is charged"
},
"logoPlacement": {
"enum": [
"auto",
"overlay",
"in_scene",
"none"
],
"type": "string",
"description": "overlay: real logo file laid flat. in_scene: the scene leaves a blank patch on the object and the real file is printed onto it (perspective, light). Never drawn by the model. auto (default): from the prompt"
}
}
}| audio | boolean | default true. false = render SILENT: no native model audio, no music bed, and no bed charge held or billed. This is the ONLY way to decline the automatic bed (see musicMood) — leave it alone for anything that should have sound, and do not combine it with ttsScript. |
| model | string | video model id from hermoso_capabilities; a named model is never swapped without asking. Omit to let the router pick; its default per job (jobEngines in hermoso_capabilities, from watched side-by-side tests) is seedance-2.5 for product, lifestyle, a person talking to camera and 30s single takes (cheaper engines broke the product there). A model the user picks is kept; never switch it |
| shots | array | MULTI-SHOT: one clip cut into these shots, in order; the seconds add up to a length the model renders. Replaces `prompt`. Only models with multiShot true in hermoso_capabilities; a shot prompt over that model’s shotPromptMaxChars (Kling 3.0: 512) is refused free, never cut. |
| speak | string | A PERSON SAYING THESE EXACT WORDS TO CAMERA — the default for any "make my photo talk" / spokesperson / talk-to-camera ask. Pass with `creator` or a portrait as `refImage`: the video model films them saying it in their own voice, matched to who they are, and the length follows the words (leave durationSeconds out). `prompt` is then the staging (e.g. "natural", "walking in a park"). Real footage, not an animated photo; generate_avatar is the animated-photo look, only when asked for by name |
| angles | boolean | OPTIONAL, OFF BY DEFAULT: with `creator`, also send the extra views that creator already has saved (their pose plates, up to 2) beside their portrait. A test showed no visible improvement over the portrait alone, and each extra view adds about 25 s before the render starts, so leave it off unless asked. Views are skipped when the face library is busy, and the render always goes ahead on the portrait. |
| dryRun | boolean | true = render NOTHING and return the credits this exact clip would hold (an estimate; the final charge is its actual cost). Free. Call this to price a clip before running it: without it the call renders and bills. |
| extend | boolean | true = EXTEND refVideo by durationSeconds (default 5) with what your prompt says happens next, as ONE clip. By default it CUTS to a NEW SHOT: new angle, same scene, room sound carried, no music at the cut. A named model (any i2v) renders it. |
| prompt | string | the video prompt / shot description (for a refVideo edit, this is the transformation instruction); optional with `shots` |
| creator | string | STAR A SAVED CREATOR in this clip — their id from list_creators, or the name you know them by, or a PRESET AI creator from list_creators presets (exact name or id; free, no generation). Their saved portrait rides first among the references as the on-camera person, with their saved consent, exactly as render_ad casts them; a real person saved from a photo keeps their real face on camera. An unknown name is refused by name, nothing charged. |
| endImage | string | local path or URL of the LAST frame: the clip travels from refImage (required with it) to this image. Only models with endFrame true in hermoso_capabilities take it; any other named model is refused by name, nothing charged. |
| refImage | string | local path or URL to anchor the first frame. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded. |
| refVideo | string | URL of an existing video to EDIT rather than generate from scratch — the clip is transformed per your prompt, keeping the SOURCE clip’s aspect ratio and, on models hermoso_capabilities marks `sourceLength`, its LENGTH (their `durations` are the per-second price ladder; a durationSeconds sent is reported as unused). Trim the source to change the length. Omit `model` for the default editor, or name a model whose videoEdit is true in hermoso_capabilities (a named model that cannot edit is refused, nothing charged); refImage rides along as the look of what the edit adds. With extend:true this is instead the clip to EXTEND. Omit to generate a fresh clip. |
| ttsVoice | string | voice name, e.g. Rachel / George |
| faceRoute | string | 'face_lane' records the user's statement "I own the rights to this face" for the real person in their reference (paid plans; the likeness terms apply). Set it only when the user picked that option after a render said it could not use that face, and send the same request again; it then renders through Hermoso's face library at the normal price. Never set it on your own. The other options are a different `model` or one of the user's saved `creator`s. |
| musicMood | string | the mood of the music bed (upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic; default warm). It does not decide WHETHER there is one: a clip with no audio track (a "silent" model in hermoso_capabilities, or one that came back mute) gets a bed composed and CHARGED automatically at explainerMusicCredits. audio:false = silent, no bed, no charge. |
| refImages | array | SEVERAL reference images (paths or URLs: a person, products, a place) that must all appear. A model uses up to its `refs.max` in hermoso_capabilities (one-image models take the first); with `refs.promptAddressed`, name them Image 1, Image 2… in order. minimax-h3-max-ref takes 9 and keeps each as a reference, not a first frame. |
| ttsScript | string | voiceover script to speak |
| cameraMove | string | A named camera move around the still in refImage (orbit = a quarter turn, the default; orbit_half / orbit_full = a half turn / a turntable). Only minimax-h3-max-camera renders one: with model omitted it is picked; any other named model is refused, nothing charged. Needs refImage. |
| extendMode | string | with extend: 'continue' = the SAME take past its last frame, only when asked seamless (models with extend in hermoso_capabilities). |
| musicModel | string | compose the bed on minimax-music-3 or lyria-3.5, skipping the free library |
| resolution | string | Omit = the default tier, hermoso_capabilities defaultResolution (720p), the tier its `credits` prices are for. 1080p is ~2.25x, 4k more, 480p less: quote creditsByRes and pass the tier you quoted. A tier a model's `resolutions` lacks renders at its best one. |
| aspectRatio | string | default '9:16' |
| interactionId | string | with extend:true on an Omni model: the interactionId returned by an earlier render on that model — continues it from its own stored context instead of re-uploading refVideo. |
| durationSeconds | number | length of THIS ONE clip in seconds: one of the CHOSEN model’s listed durations in hermoso_capabilities (they differ per model, 4–8s on the short ones up to 30s). One continuous generation, so a longer ask than the model’s longest clip is REFUSED, nothing charged (never quietly truncated). Past ~15s NAME a long-clip model in `model`. For a longer spot use plan_ad with durationSeconds then render_ad, which stitches acts (on a 15s model, 40s = 15+15+10). |
| cameraTrajectory | array | Your own ordered camera path, 2 to 12 keyframes, for the camera-controls model only (same rule as cameraMove; overrides it). The first pose is held until its time and the last pose is held to the end. A value outside these bounds is refused by name, nothing charged. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"raw": {
"type": "boolean",
"description": "Send the prompt to the model exactly as written, without the wording Hermoso usually adds (packaging and label guidance, reference lines, colour names for hex codes, quality hints). Use it when you want the model's own interpretation rather than Hermoso's render craft. Two vendor-required fixes still apply: extra @ImageN tokens are dropped and an over-long prompt is trimmed at a sentence (on Veo it is refused with the length and the cap instead). Billing, durable delivery and per-model validation are unchanged."
},
"loop": {
"type": "boolean",
"description": "true = a seamless loop whose last frame flows back into its first. Only models with loop true in hermoso_capabilities; needs refImage and cannot be combined with endImage."
},
"audio": {
"type": "boolean",
"description": "default true. false = render SILENT: no native model audio, no music bed, and no bed charge held or billed. This is the ONLY way to decline the automatic bed (see musicMood) — leave it alone for anything that should have sound, and do not combine it with ttsScript."
},
"model": {
"type": "string",
"description": "video model id from hermoso_capabilities; a named model is never swapped without asking. Omit to let the router pick; its default per job (jobEngines in hermoso_capabilities, from watched side-by-side tests) is seedance-2.5 for product, lifestyle, a person talking to camera and 30s single takes (cheaper engines broke the product there). A model the user picks is kept; never switch it"
},
"shots": {
"type": "array",
"items": {
"type": "object",
"required": [
"prompt",
"seconds"
],
"properties": {
"prompt": {
"type": "string",
"description": "what happens in this shot"
},
"seconds": {
"type": "integer",
"maximum": 15,
"minimum": 1,
"description": "this shot’s length in whole seconds"
}
}
},
"description": "MULTI-SHOT: one clip cut into these shots, in order; the seconds add up to a length the model renders. Replaces `prompt`. Only models with multiShot true in hermoso_capabilities; a shot prompt over that model’s shotPromptMaxChars (Kling 3.0: 512) is refused free, never cut."
},
"speak": {
"type": "string",
"description": "A PERSON SAYING THESE EXACT WORDS TO CAMERA — the default for any \"make my photo talk\" / spokesperson / talk-to-camera ask. Pass with `creator` or a portrait as `refImage`: the video model films them saying it in their own voice, matched to who they are, and the length follows the words (leave durationSeconds out). `prompt` is then the staging (e.g. \"natural\", \"walking in a park\"). Real footage, not an animated photo; generate_avatar is the animated-photo look, only when asked for by name"
},
"angles": {
"type": "boolean",
"description": "OPTIONAL, OFF BY DEFAULT: with `creator`, also send the extra views that creator already has saved (their pose plates, up to 2) beside their portrait. A test showed no visible improvement over the portrait alone, and each extra view adds about 25 s before the render starts, so leave it off unless asked. Views are skipped when the face library is busy, and the render always goes ahead on the portrait."
},
"dryRun": {
"type": "boolean",
"description": "true = render NOTHING and return the credits this exact clip would hold (an estimate; the final charge is its actual cost). Free. Call this to price a clip before running it: without it the call renders and bills."
},
"extend": {
"type": "boolean",
"description": "true = EXTEND refVideo by durationSeconds (default 5) with what your prompt says happens next, as ONE clip. By default it CUTS to a NEW SHOT: new angle, same scene, room sound carried, no music at the cut. A named model (any i2v) renders it."
},
"prompt": {
"type": "string",
"description": "the video prompt / shot description (for a refVideo edit, this is the transformation instruction); optional with `shots`"
},
"creator": {
"type": "string",
"description": "STAR A SAVED CREATOR in this clip — their id from list_creators, or the name you know them by, or a PRESET AI creator from list_creators presets (exact name or id; free, no generation). Their saved portrait rides first among the references as the on-camera person, with their saved consent, exactly as render_ad casts them; a real person saved from a photo keeps their real face on camera. An unknown name is refused by name, nothing charged."
},
"endImage": {
"type": "string",
"description": "local path or URL of the LAST frame: the clip travels from refImage (required with it) to this image. Only models with endFrame true in hermoso_capabilities take it; any other named model is refused by name, nothing charged."
},
"refImage": {
"type": "string",
"description": "local path or URL to anchor the first frame. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
},
"refVideo": {
"type": "string",
"description": "URL of an existing video to EDIT rather than generate from scratch — the clip is transformed per your prompt, keeping the SOURCE clip’s aspect ratio and, on models hermoso_capabilities marks `sourceLength`, its LENGTH (their `durations` are the per-second price ladder; a durationSeconds sent is reported as unused). Trim the source to change the length. Omit `model` for the default editor, or name a model whose videoEdit is true in hermoso_capabilities (a named model that cannot edit is refused, nothing charged); refImage rides along as the look of what the edit adds. With extend:true this is instead the clip to EXTEND. Omit to generate a fresh clip."
},
"ttsVoice": {
"type": "string",
"description": "voice name, e.g. Rachel / George"
},
"faceRoute": {
"enum": [
"face_lane"
],
"type": "string",
"description": "'face_lane' records the user's statement \"I own the rights to this face\" for the real person in their reference (paid plans; the likeness terms apply). Set it only when the user picked that option after a render said it could not use that face, and send the same request again; it then renders through Hermoso's face library at the normal price. Never set it on your own. The other options are a different `model` or one of the user's saved `creator`s."
},
"musicMood": {
"type": "string",
"description": "the mood of the music bed (upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic; default warm). It does not decide WHETHER there is one: a clip with no audio track (a \"silent\" model in hermoso_capabilities, or one that came back mute) gets a bed composed and CHARGED automatically at explainerMusicCredits. audio:false = silent, no bed, no charge."
},
"refImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "SEVERAL reference images (paths or URLs: a person, products, a place) that must all appear. A model uses up to its `refs.max` in hermoso_capabilities (one-image models take the first); with `refs.promptAddressed`, name them Image 1, Image 2… in order. minimax-h3-max-ref takes 9 and keeps each as a reference, not a first frame."
},
"ttsScript": {
"type": "string",
"description": "voiceover script to speak"
},
"cameraMove": {
"enum": [
"orbit",
"orbit_left",
"orbit_half",
"orbit_full",
"rise",
"crane_up",
"push_in",
"pull_back",
"reveal"
],
"type": "string",
"description": "A named camera move around the still in refImage (orbit = a quarter turn, the default; orbit_half / orbit_full = a half turn / a turntable). Only minimax-h3-max-camera renders one: with model omitted it is picked; any other named model is refused, nothing charged. Needs refImage."
},
"extendMode": {
"enum": [
"cut",
"continue"
],
"type": "string",
"description": "with extend: 'continue' = the SAME take past its last frame, only when asked seamless (models with extend in hermoso_capabilities)."
},
"musicModel": {
"type": "string",
"description": "compose the bed on minimax-music-3 or lyria-3.5, skipping the free library"
},
"resolution": {
"enum": [
"480p",
"720p",
"1080p",
"4k"
],
"type": "string",
"description": "Omit = the default tier, hermoso_capabilities defaultResolution (720p), the tier its `credits` prices are for. 1080p is ~2.25x, 4k more, 480p less: quote creditsByRes and pass the tier you quoted. A tier a model's `resolutions` lacks renders at its best one."
},
"aspectRatio": {
"type": "string",
"description": "default '9:16'"
},
"interactionId": {
"type": "string",
"description": "with extend:true on an Omni model: the interactionId returned by an earlier render on that model — continues it from its own stored context instead of re-uploading refVideo."
},
"durationSeconds": {
"type": "number",
"description": "length of THIS ONE clip in seconds: one of the CHOSEN model’s listed durations in hermoso_capabilities (they differ per model, 4–8s on the short ones up to 30s). One continuous generation, so a longer ask than the model’s longest clip is REFUSED, nothing charged (never quietly truncated). Past ~15s NAME a long-clip model in `model`. For a longer spot use plan_ad with durationSeconds then render_ad, which stitches acts (on a 15s model, 40s = 15+15+10)."
},
"cameraTrajectory": {
"type": "array",
"items": {
"type": "object",
"required": [
"time",
"azimuth",
"elevation",
"distance"
],
"properties": {
"time": {
"type": "number",
"maximum": 1,
"minimum": 0,
"description": "when this pose is reached, 0 = start of the clip, 1 = end"
},
"azimuth": {
"type": "number",
"description": "horizontal angle around the subject in degrees (0 = where the still was taken; the sign turns the camera the other way; at most 32 full turns of total travel)"
},
"distance": {
"type": "number",
"description": "distance from the subject in scene units, 1 = the distance of the still; smaller is closer",
"exclusiveMinimum": 0
},
"elevation": {
"type": "number",
"maximum": 90,
"minimum": -90,
"description": "vertical angle in degrees, -90 (below) to 90 (straight above)"
}
}
},
"maxItems": 12,
"minItems": 2,
"description": "Your own ordered camera path, 2 to 12 keyframes, for the camera-controls model only (same rule as cameraMove; overrides it). The first pose is held until its time and the last pose is held to the end. A value outside these bounds is refused by name, nothing charged."
}
}
}| engine |
| string |
| voice-engine id: one voice defaults to 'seed-audio'; a dialogue defaults to the dialogue model ('eleven-dialogue'); or 'eleven-v4', 'minimax-speech', 'kokoro'… (hermoso_capabilities) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"text": {
"type": "string",
"description": "ONE VOICE: the script to speak (≤900 characters)"
},
"lines": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "what they say"
},
"voice": {
"type": "string",
"description": "that speaker's voice from the engine's list (or an ElevenLabs voice id); omit to be given one"
},
"speaker": {
"type": "string",
"description": "who speaks; the same name keeps the same voice"
}
}
},
"description": "DIALOGUE: the turns in speaking order"
},
"voice": {
"type": "string",
"description": "ONE VOICE: a preset from the chosen engine (e.g. 'Aria'/'George' on eleven-v4, 'stokie_en' on seed-audio); omit for the engine default"
},
"dryRun": {
"type": "boolean",
"description": "true = the exact credits only, nothing made or charged"
},
"engine": {
"type": "string",
"description": "voice-engine id: one voice defaults to 'seed-audio'; a dialogue defaults to the dialogue model ('eleven-dialogue'); or 'eleven-v4', 'minimax-speech', 'kokoro'… (hermoso_capabilities)"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"image"
],
"properties": {
"brief": {
"type": "string",
"description": "what to test, e.g. \"price-led vs outcome-led\" or \"speak to busy parents\""
},
"count": {
"type": "integer",
"maximum": 10,
"minimum": 1,
"description": "how many headlines to write when `headlines` is omitted (default 5)"
},
"image": {
"type": "string",
"description": "the finished static ad: URL, Library item URL, upload_file URL or local path"
},
"fixLabel": {
"type": "boolean",
"description": "false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)"
},
"headlines": {
"type": "array",
"items": {
"type": "string"
},
"description": "your own headlines to test (up to 10); omit to have them written"
}
}
}| string |
| anything the hooks must respect, e.g. "keep it calm", "show the product in every hook" |
| video* | string | the finished video to give new hooks: its served file URL |
| dryRun | boolean | true = the paid plan and the price, no render |
| useBrand | boolean | false = send no brand name or product photo (for a video that is not this workspace brand’s) |
| quoteOnly | boolean | true = the exact credits, free; nothing runs |
| resolution | string | render tier for the new opening. Defaults to the source’s OWN tier so the hook matches the rest of the ad; a lower tier costs a lot less and is scaled into the source’s canvas (visibly softer for the first seconds). dryRun quotes whichever you pick. |
| hookSeconds | number | where the CURRENT hook ends, in seconds (1.5-4). Omit to use the first shot cut. |
| productImage | string | product photo URL used as a reference in hooks that show the product (defaults to the workspace brand’s first product photo) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video"
],
"properties": {
"plan": {
"description": "the `plan` object a previous dryRun returned, to render exactly those hooks without planning again"
},
"count": {
"type": "number",
"description": "how many hook versions, 1-5 (default 3)"
},
"hooks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "viral hook to join in front: a post link or a video file"
},
"cutAt": {
"type": "number",
"description": "url: override the found payoff cut"
},
"start": {
"type": "number",
"description": "url: skip the video’s own first seconds"
},
"prompt": {
"type": "string",
"description": "an opening described in words"
},
"mechanic": {
"type": "string",
"description": "a named hook mechanic id from list_hooks"
}
}
},
"description": "your own openings, one version each"
},
"notes": {
"type": "string",
"description": "anything the hooks must respect, e.g. \"keep it calm\", \"show the product in every hook\""
},
"video": {
"type": "string",
"description": "the finished video to give new hooks: its served file URL"
},
"dryRun": {
"type": "boolean",
"description": "true = the paid plan and the price, no render"
},
"useBrand": {
"type": "boolean",
"description": "false = send no brand name or product photo (for a video that is not this workspace brand’s)"
},
"quoteOnly": {
"type": "boolean",
"description": "true = the exact credits, free; nothing runs"
},
"resolution": {
"enum": [
"480p",
"720p",
"1080p"
],
"type": "string",
"description": "render tier for the new opening. Defaults to the source’s OWN tier so the hook matches the rest of the ad; a lower tier costs a lot less and is scaled into the source’s canvas (visibly softer for the first seconds). dryRun quotes whichever you pick."
},
"hookSeconds": {
"type": "number",
"description": "where the CURRENT hook ends, in seconds (1.5-4). Omit to use the first shot cut."
},
"productImage": {
"type": "string",
"description": "product photo URL used as a reference in hooks that show the product (defaults to the workspace brand’s first product photo)"
}
}
}| default 720p (rendered at 768p); '480p' is cheaper |
| resumeTime* | number | seconds into the ORIGINAL clip where it picks up again after the new scene (2.625-60, at or after startTime) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video",
"startTime",
"resumeTime"
],
"properties": {
"video": {
"type": "string",
"description": "the source video URL (a render, job result or list_library item)"
},
"prompt": {
"type": "string",
"description": "what happens in the new scene: the action, the subject, the camera"
},
"seconds": {
"type": "number",
"description": "length of the new scene, 5-13 seconds (default 5)"
},
"refImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 4 picture URLs of what should appear (the product, a person, a place)"
},
"refVideos": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 1 clip URL whose look or motion the scene follows"
},
"startTime": {
"type": "number",
"description": "seconds into the clip where the new scene starts (1.625-60)"
},
"resolution": {
"enum": [
"480p",
"720p"
],
"type": "string",
"description": "default 720p (rendered at 768p); '480p' is cheaper"
},
"resumeTime": {
"type": "number",
"description": "seconds into the ORIGINAL clip where it picks up again after the new scene (2.625-60, at or after startTime)"
}
}
}| include trashed files (default false) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"query": {
"type": "string",
"description": "only files whose name contains this"
},
"folderId": {
"type": "string",
"description": "list the contents of this folder id"
},
"pageSize": {
"type": "number",
"description": "rows per page (1–200, default 50)"
},
"pageToken": {
"type": "string",
"description": "cursor from a previous call"
},
"recursive": {
"type": "boolean",
"description": "with folderId: the whole tree"
},
"onlyFolders": {
"type": "boolean",
"description": "list folders only"
},
"includeTrashed": {
"type": "boolean",
"description": "include trashed files (default false)"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"tier": {
"enum": [
"luxury",
"premium",
"drugstore"
],
"type": "string",
"description": "product tier, used with category — changes the FINISH of the room, never the room. Default premium."
},
"channel": {
"type": "string",
"description": "restrict the performance half to one channel (facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky)"
},
"category": {
"type": "string",
"description": "the product category (e.g. 'skincare serum', 'protein powder', 'sunglasses') — returns the setting our Location x Tier matrix puts that category in, with the reason"
},
"authentic": {
"type": "boolean",
"description": "true if the planned ad is an authentic/UGC/creator-register render — on-screen-text hooks are then reported unusable, with the reason"
}
}
}| account | string | which Instagram account — an @handle or id from list_connector_accounts("instagram"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account. |
| includeUnpublished | boolean | Facebook only — also return unpublished drafts (hidden by default) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brand": {
"type": "string",
"description": "WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done."
},
"limit": {
"type": "number",
"description": "how many posts (default 25, max 100)"
},
"cursor": {
"type": "string",
"description": "paging cursor returned by a previous call"
},
"pageId": {
"type": "string",
"description": "which connected Page — omit when the profile has only one"
},
"tagged": {
"type": "boolean",
"description": "INSTAGRAM — list posts OTHER accounts tagged the brand in (UGC, creator posts, partner mentions), with each author’s @handle, instead of the brand’s own posts. Free."
},
"target": {
"enum": [
"facebook",
"instagram"
],
"type": "string",
"description": "default facebook; 'instagram' reads the Page's linked IG business account"
},
"account": {
"type": "string",
"description": "which Instagram account — an @handle or id from list_connector_accounts(\"instagram\"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account."
},
"includeUnpublished": {
"type": "boolean",
"description": "Facebook only — also return unpublished drafts (hidden by default)"
}
}
}| describe |
| string |
| your own words: added to the look, or the custom choice |
| aspectRatio | string | one of its listed shapes |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"text": {
"type": "string",
"description": "a line printed exactly, on an effect that takes one"
},
"still": {
"type": "string",
"description": "CLIP STEP: the finished picture to animate"
},
"effect": {
"type": "string",
"description": "effect id from the list"
},
"images": {
"type": "array",
"items": {
"type": "string"
},
"description": "its pictures, in the order listed: URL, local path or Library id"
},
"speech": {
"type": "string",
"description": "CLIP STEP: a line said in the clip, on an effect that speaks"
},
"confirm": {
"type": "boolean",
"description": "true = run it (real spend); without it, a free quote"
},
"options": {
"type": "object",
"description": "its listed options, e.g. {material:\"knit\"}; \"custom\" means use `describe`",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"describe": {
"type": "string",
"description": "your own words: added to the look, or the custom choice"
},
"aspectRatio": {
"type": "string",
"description": "one of its listed shapes"
}
}
}| Argument | Type | Description |
|---|---|---|
| lane | string | 'blocks' (default) = 10-second video blocks, real motion, one narrated line per block; 'stills' = the picture film (a still about every 1.5s, narrated, no video model; cheaper). Quote either with dryRun. |
| music | string | music bed under the narration, measured to sit about 14 dB under the voice and sidechain-ducked beneath it. Omit and the KIDS and FAIRYTALE channels get their recommended bed COMPOSED for this film — those two are the only channels a bed is due on unasked, and it costs a small flat fee; every other channel ships dry. 'off' forces silence. 'library' takes a free curated track only, and ships dry when none is on file. NAME A MOOD (upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic) or DESCRIBE it in words to compose one on ANY channel, at the same fee. hermoso_capabilities reports the exact figure as explainerMusicCredits; quote it before you turn a bed on or pick a mood. |
| style | string | visual style: 'cinematic' (default, photoreal); styled shortcuts editorial_collage, flat_vector, stickman, whiteboard, ink_marker, silhouette, storybook, paper_diorama, isometric, claymation, pixel_art, watercolor, fluffy_toy, low_poly, stylized_3d, studio_3d (the Kids default), mannequin; or ANY look described in words ('80s anime cel animation'), locked across every frame. Ask rather than pick silently; a styled look costs more. |
| topic | string | what the explainer should teach or explain — a topic or a short brief (host_episode: or pass `script`) |
| voice | string | narration voice name — omit for the default warm read |
| dryRun | boolean | true = return the estimated credits this explainer reserves (its own pricing, stopped at the hold) and render nothing. Quote it before running one; try frameDensity lean or minimal when the balance is short. |
| format | string | blocks lane: 'explainer' (default; Gemini Omni 720p, 16:9 or 9:16, runs exactly the length asked) or 'faceless_channel' (a YouTube/TikTok faceless channel video; MiniMax H3 at 2K, 16:9 by default, five hard cuts per 10s block, runs exactly the length asked: a last part-block is rendered whole and cut). Omit and a history / kids / fairytale channel is a faceless channel video. 'host_episode' = the AI host episode (see the top). |
| script | string | host_episode: the exact words, said verbatim and split at natural breaks into 4-30s pieces |
| cameras | array | host_episode: the rotation, ids frontal / three_quarter / close or framings in words; one entry = one fixed camera |
| channel | string | the CHANNEL TYPE — it sets the pacing, the narration register and the default look, and is orthogonal to `style` (a named style always wins): explainer (casual second-person, fast cuts), history (witty chronological retelling / documentary), kids (fastest, question-first, warm teacher), fairytale (slow, atmospheric myth or folklore). Default 'explainer'. |
| creator | string | host_episode: the host, a saved creator or a preset by name or id (list_creators) |
| endCard | boolean | append the branded end card. DEFAULT FALSE — set true ONLY when the user asks for one |
| setting | string | host_episode: the set in words (default: written to fit the topic) |
| upscale | number | optional FINAL upscale — 2 doubles each side, 4 quadruples. Captions and the end card are burned BEFORE it so they upscale with the frame. It is priced BY LENGTH and it is the expensive part — several times the cost of rendering the film itself. hermoso_capabilities reports the exact figures per length as explainerUpscaleCredits. Never turn it on unasked: quote the number and let the user choose. |
| captions | boolean | turn ON-SCREEN TEXT on. DEFAULT FALSE, and leave it false unless the user asks — the narration already says the point and the pictures carry it, so the clean film is the better default. `captions:true` on its own burns SUBTITLES (see below), because that is what a caption is for: showing what is being said when the phone is on mute. Slim white text (CAPS in the stills lane), thin black outline, bottom safe band, no plate, no box. |
| fromFilm | string | A PRESENTER ON A FINISHED FILM (only when the user asks for one): the job id of a finished video-blocks explainer or faceless-channel film. With `presenter`, that film comes back with a presenter speaking each block's line on screen; every block keeps its own picture and the film without the presenter is kept (cleanVideo). Never add a presenter unasked. |
| brandName | string | brand name for the end card — omit to leave it unbranded |
| fromDraft | string | host_episode: a finished draft's job id, to render it in HD (720p) with the same script, cameras, set and host |
| hostImage | string | host_episode: a photo URL of the host instead (upload_file for a local file); a real person's face needs a paid plan |
| presenter | object | with fromFilm: who presents and how. A face the video model flags stops it, nothing charged, with the choices to offer. |
| subtitles | boolean | which on-screen text, once `captions` is on. LEAVE IT UNSET (or true) for SUBTITLES — every spoken word, in order, timed to the narration; no extra render, and there is NO cue limit, so the whole film is subtitled however long it runs (at most 4 words and one line a cue; 5 words / 32 characters in stills). Set it FALSE only if the user explicitly wants section HEADINGS instead: one short summary label held over each ~7-15s section. That is NOT what is being said — it is a label about it — so it is the wrong answer to "add captions" and to anyone watching on mute. `subtitles:true` also implies `captions:true`. TIMING: in the default blocks lane each cue is timed to its own spoken words (one listen to the narration, a tiny charge inside the quote; per-block timing, said, if it fails). In the stills lane cues are spread over each section’s measured narration by character count, exact at section edges, a few tenths off within one: it is not a word-level speech clock, so never promise frame-accurate sync there. |
| thumbnail | boolean | host_episode: one thumbnail of the host (default true) |
| aspectRatio | string | '9:16' default (a faceless channel video and a host episode default to 16:9) |
| frameDensity | string | how many pictures per second of narration, and therefore what it costs. 'standard' (default) is a frame about every 1.5s — the density a stills film needs to read as a film rather than a slideshow; 'lean' is one about every 2.5s (the longest hold that still reads as a film, ~40% of the frames and ~40% of the cost); 'minimal' is one picture about every 3.5s, the cheapest and the longest any still is ever held, and it reads close to a slideshow. Only drop below the default if the user asked for something cheaper. |
| durationSeconds | number | target length 20-120s (default 60); drives the section count — ~10s of narration each, 3-8 sections |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"lane": {
"enum": [
"blocks",
"stills"
],
"type": "string",
"description": "'blocks' (default) = 10-second video blocks, real motion, one narrated line per block; 'stills' = the picture film (a still about every 1.5s, narrated, no video model; cheaper). Quote either with dryRun."
},
"music": {
"type": "string",
"description": "music bed under the narration, measured to sit about 14 dB under the voice and sidechain-ducked beneath it. Omit and the KIDS and FAIRYTALE channels get their recommended bed COMPOSED for this film — those two are the only channels a bed is due on unasked, and it costs a small flat fee; every other channel ships dry. 'off' forces silence. 'library' takes a free curated track only, and ships dry when none is on file. NAME A MOOD (upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic) or DESCRIBE it in words to compose one on ANY channel, at the same fee. hermoso_capabilities reports the exact figure as explainerMusicCredits; quote it before you turn a bed on or pick a mood."
},
"style": {
"type": "string",
"description": "visual style: 'cinematic' (default, photoreal); styled shortcuts editorial_collage, flat_vector, stickman, whiteboard, ink_marker, silhouette, storybook, paper_diorama, isometric, claymation, pixel_art, watercolor, fluffy_toy, low_poly, stylized_3d, studio_3d (the Kids default), mannequin; or ANY look described in words ('80s anime cel animation'), locked across every frame. Ask rather than pick silently; a styled look costs more."
},
"topic": {
"type": "string",
"description": "what the explainer should teach or explain — a topic or a short brief (host_episode: or pass `script`)"
},
"voice": {
"type": "string",
"description": "narration voice name — omit for the default warm read"
},
"dryRun": {
"type": "boolean",
"description": "true = return the estimated credits this explainer reserves (its own pricing, stopped at the hold) and render nothing. Quote it before running one; try frameDensity lean or minimal when the balance is short."
},
"format": {
"enum": [
"explainer",
"faceless_channel",
"host_episode"
],
"type": "string",
"description": "blocks lane: 'explainer' (default; Gemini Omni 720p, 16:9 or 9:16, runs exactly the length asked) or 'faceless_channel' (a YouTube/TikTok faceless channel video; MiniMax H3 at 2K, 16:9 by default, five hard cuts per 10s block, runs exactly the length asked: a last part-block is rendered whole and cut). Omit and a history / kids / fairytale channel is a faceless channel video. 'host_episode' = the AI host episode (see the top)."
},
"script": {
"type": "string",
"description": "host_episode: the exact words, said verbatim and split at natural breaks into 4-30s pieces"
},
"cameras": {
"type": "array",
"items": {
"type": "string"
},
"description": "host_episode: the rotation, ids frontal / three_quarter / close or framings in words; one entry = one fixed camera"
},
"channel": {
"enum": [
"explainer",
"history",
"kids",
"fairytale"
],
"type": "string",
"description": "the CHANNEL TYPE — it sets the pacing, the narration register and the default look, and is orthogonal to `style` (a named style always wins): explainer (casual second-person, fast cuts), history (witty chronological retelling / documentary), kids (fastest, question-first, warm teacher), fairytale (slow, atmospheric myth or folklore). Default 'explainer'."
},
"creator": {
"type": "string",
"description": "host_episode: the host, a saved creator or a preset by name or id (list_creators)"
},
"endCard": {
"type": "boolean",
"description": "append the branded end card. DEFAULT FALSE — set true ONLY when the user asks for one"
},
"setting": {
"type": "string",
"description": "host_episode: the set in words (default: written to fit the topic)"
},
"upscale": {
"type": "number",
"description": "optional FINAL upscale — 2 doubles each side, 4 quadruples. Captions and the end card are burned BEFORE it so they upscale with the frame. It is priced BY LENGTH and it is the expensive part — several times the cost of rendering the film itself. hermoso_capabilities reports the exact figures per length as explainerUpscaleCredits. Never turn it on unasked: quote the number and let the user choose."
},
"captions": {
"type": "boolean",
"description": "turn ON-SCREEN TEXT on. DEFAULT FALSE, and leave it false unless the user asks — the narration already says the point and the pictures carry it, so the clean film is the better default. `captions:true` on its own burns SUBTITLES (see below), because that is what a caption is for: showing what is being said when the phone is on mute. Slim white text (CAPS in the stills lane), thin black outline, bottom safe band, no plate, no box."
},
"fromFilm": {
"type": "string",
"description": "A PRESENTER ON A FINISHED FILM (only when the user asks for one): the job id of a finished video-blocks explainer or faceless-channel film. With `presenter`, that film comes back with a presenter speaking each block's line on screen; every block keeps its own picture and the film without the presenter is kept (cleanVideo). Never add a presenter unasked."
},
"brandName": {
"type": "string",
"description": "brand name for the end card — omit to leave it unbranded"
},
"fromDraft": {
"type": "string",
"description": "host_episode: a finished draft's job id, to render it in HD (720p) with the same script, cameras, set and host"
},
"hostImage": {
"type": "string",
"description": "host_episode: a photo URL of the host instead (upload_file for a local file); a real person's face needs a paid plan"
},
"presenter": {
"type": "object",
"properties": {
"look": {
"enum": [
"corner",
"badge",
"split"
],
"type": "string",
"description": "'corner' (default): cut out of a green screen, standing in a bottom corner; 'badge': a round badge in a corner; 'split': the block on one half, the presenter on the other"
},
"image": {
"type": "string",
"description": "or a photo / avatar of the presenter (upload_file for a local file); a real person's face follows the real-face rules"
},
"scale": {
"type": "number",
"description": "the share of the frame the presenter takes, 0.2-0.8 (default: the look's own size)"
},
"voice": {
"type": "string",
"description": "the presenter's voice (default: the film's own narrator)"
},
"creator": {
"type": "string",
"description": "a saved creator or a preset, by name or id (list_creators)"
},
"position": {
"type": "string",
"description": "'br' (default), 'bl', 'tr' or 'tl' (a corner cutout stays at the bottom); for split, 'bl' puts the presenter first"
}
},
"description": "with fromFilm: who presents and how. A face the video model flags stops it, nothing charged, with the choices to offer."
},
"subtitles": {
"type": "boolean",
"description": "which on-screen text, once `captions` is on. LEAVE IT UNSET (or true) for SUBTITLES — every spoken word, in order, timed to the narration; no extra render, and there is NO cue limit, so the whole film is subtitled however long it runs (at most 4 words and one line a cue; 5 words / 32 characters in stills). Set it FALSE only if the user explicitly wants section HEADINGS instead: one short summary label held over each ~7-15s section. That is NOT what is being said — it is a label about it — so it is the wrong answer to \"add captions\" and to anyone watching on mute. `subtitles:true` also implies `captions:true`. TIMING: in the default blocks lane each cue is timed to its own spoken words (one listen to the narration, a tiny charge inside the quote; per-block timing, said, if it fails). In the stills lane cues are spread over each section’s measured narration by character count, exact at section edges, a few tenths off within one: it is not a word-level speech clock, so never promise frame-accurate sync there."
},
"thumbnail": {
"type": "boolean",
"description": "host_episode: one thumbnail of the host (default true)"
},
"aspectRatio": {
"enum": [
"9:16",
"16:9",
"1:1",
"4:5",
"3:4"
],
"type": "string",
"description": "'9:16' default (a faceless channel video and a host episode default to 16:9)"
},
"frameDensity": {
"enum": [
"standard",
"lean",
"minimal"
],
"type": "string",
"description": "how many pictures per second of narration, and therefore what it costs. 'standard' (default) is a frame about every 1.5s — the density a stills film needs to read as a film rather than a slideshow; 'lean' is one about every 2.5s (the longest hold that still reads as a film, ~40% of the frames and ~40% of the cost); 'minimal' is one picture about every 3.5s, the cheapest and the longest any still is ever held, and it reads close to a slideshow. Only drop below the default if the user asked for something cheaper."
},
"durationSeconds": {
"type": "number",
"description": "target length 20-120s (default 60); drives the section count — ~10s of narration each, 3-8 sections"
}
}
}| brief | string | the seller's direction for the whole set: a mood, a setting, an audience |
| image | string | the product photo when it is not in the catalog: an https or upload_file URL, a Library id (L0, L1…) or a local path on the CLI |
| inBox | array | what ships in the box (for a whats_in_box shot); nothing else is ever drawn in it |
| model | string | image model id from hermoso_capabilities; omit for the default |
| shots | array | the images, in order: built-in main, lifestyle, detail, scale, in_use, multi_angle, whats_in_box, back_of_pack, or any shot in your own words. Default: main, lifestyle, detail, scale, in_use. main always comes first when asked |
| title | string | the product name, when it is not in the catalog |
| copyIn | string | A+ words: 'fields' (default) = suggested for Amazon's text fields, none painted; 'image' = the words painted on the picture |
| dryRun | boolean | true = plan and quote only, nothing rendered or charged |
| brandId | string | a profile id/name from list_brands; omit for the active profile (passing it pins it like use_brand) |
| mainJob | string | the mainJob id a previous call returned, when rendering its `remaining`: they are drawn from that main |
| product | string | the product name or id from list_products: its catalog photo and its own facts are used |
| mainImage | string | an APPROVED main image (URL or Library id) to draw every other image from instead of rendering a new main |
| aspectRatio | string | the canvas for the whole set; default 1:1 (the marketplaces' square) |
| description | string | what the product is, when it is not in the catalog |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"copy": {
"description": "the seller's own words for A+ text fields, by module id then field, e.g. {aplus_hero:{headline:'…'}}; each is clipped to Amazon's limit. Without it the fields are suggested only from the product's own data, never invented"
},
"plan": {
"type": "array",
"items": {},
"description": "the `plan` (or `remaining`) a previous call returned: render exactly those images, no new plan, with the same image"
},
"aplus": {
"type": "boolean",
"description": "also Amazon's 7 A+ modules: 1 brand hero and 7 brand promise at 21:9 (Premium full-width 1464x600 + mobile 600x450), 2-6 at 3:2 (key benefits, how to use, materials and details, lifestyle, what you get; Premium image-with-text 800x600, Basic header 970x600). With shots:[] only the A+ modules"
},
"brief": {
"type": "string",
"description": "the seller's direction for the whole set: a mood, a setting, an audience"
},
"image": {
"type": "string",
"description": "the product photo when it is not in the catalog: an https or upload_file URL, a Library id (L0, L1…) or a local path on the CLI"
},
"inBox": {
"type": "array",
"items": {
"type": "string"
},
"description": "what ships in the box (for a whats_in_box shot); nothing else is ever drawn in it"
},
"model": {
"type": "string",
"description": "image model id from hermoso_capabilities; omit for the default"
},
"shots": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 9,
"description": "the images, in order: built-in main, lifestyle, detail, scale, in_use, multi_angle, whats_in_box, back_of_pack, or any shot in your own words. Default: main, lifestyle, detail, scale, in_use. main always comes first when asked"
},
"title": {
"type": "string",
"description": "the product name, when it is not in the catalog"
},
"copyIn": {
"enum": [
"fields",
"image"
],
"type": "string",
"description": "A+ words: 'fields' (default) = suggested for Amazon's text fields, none painted; 'image' = the words painted on the picture"
},
"dryRun": {
"type": "boolean",
"description": "true = plan and quote only, nothing rendered or charged"
},
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands; omit for the active profile (passing it pins it like use_brand)"
},
"mainJob": {
"type": "string",
"description": "the mainJob id a previous call returned, when rendering its `remaining`: they are drawn from that main"
},
"product": {
"type": "string",
"description": "the product name or id from list_products: its catalog photo and its own facts are used"
},
"mainImage": {
"type": "string",
"description": "an APPROVED main image (URL or Library id) to draw every other image from instead of rendering a new main"
},
"aspectRatio": {
"enum": [
"1:1",
"4:5",
"3:4",
"4:3",
"16:9",
"9:16",
"2:3",
"3:2"
],
"type": "string",
"description": "the canvas for the whole set; default 1:1 (the marketplaces' square)"
},
"description": {
"type": "string",
"description": "what the product is, when it is not in the catalog"
}
}
}| mood |
| string |
| music and pace |
| name | string | default: the saved brand's |
| size | string | frame shape: 16:9, 9:16, 1:1 or 4:5 |
| spec | object | the whole film as JSON, from a quote or a saved film; help:true shows the spec language |
| brief | string | what the film is about, for the planner |
| lines | array | more short lines, in order |
| model | string | image-to-video model id from hermoso_capabilities; default is the best value for short shots |
| moves | array | per scene, what happens in its picture; omit and the planner writes them |
| music | string | 'off' = silent |
| theme | string | logo: dark for a light logo |
| energy | number | how much the film keeps moving, 0 to 1; 0 films exactly what is written |
| images | array | pictures, hero first (https, upload_file URL or Library id) |
| moving | boolean | true: one thing physically happens in each picture, filmed by a video model. false: stills moved by a virtual camera, far cheaper. Default true for launch, product, social |
| recipe | string | type, logo, launch, product or social; help:true lists the recipes |
| confirm | boolean | true renders the film for real credits; without it you get a free quote |
| preview | boolean | true renders a 1-credit contact sheet to approve before the full render |
| seconds | number | logo 4-10, others 6-60 |
| tagline | string | line shown under the headline or logo |
| projectUrl | string | a past film's url; returns its saved spec to edit and render again |
| resolution | string | the moving shots: 720p or 1080p (default) where the model has them |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"help": {
"type": "boolean",
"description": "true returns the spec language and recipes; renders nothing"
},
"hook": {
"type": "string",
"description": "opening line; '*word*' = italic accent"
},
"logo": {
"type": "string",
"description": "logo image: https, upload_file URL or Library id; default the brand's"
},
"mood": {
"enum": [
"elegant",
"epic",
"upbeat",
"hype",
"calm",
"tense",
"playful"
],
"type": "string",
"description": "music and pace"
},
"name": {
"type": "string",
"description": "default: the saved brand's"
},
"size": {
"enum": [
"16:9",
"9:16",
"1:1",
"4:5"
],
"type": "string",
"description": "frame shape: 16:9, 9:16, 1:1 or 4:5"
},
"spec": {
"type": "object",
"properties": {},
"description": "the whole film as JSON, from a quote or a saved film; help:true shows the spec language",
"additionalProperties": {}
},
"brief": {
"type": "string",
"description": "what the film is about, for the planner"
},
"lines": {
"type": "array",
"items": {
"type": "string"
},
"description": "more short lines, in order"
},
"model": {
"type": "string",
"description": "image-to-video model id from hermoso_capabilities; default is the best value for short shots"
},
"moves": {
"type": "array",
"items": {
"type": "object",
"required": [
"scene",
"action"
],
"properties": {
"layer": {
"type": "string",
"description": "the picture layer's id, when the scene has several"
},
"scene": {
"type": "string",
"description": "scene id"
},
"still": {
"type": "string",
"description": "a picture to animate, when not the one in the scene"
},
"video": {
"type": "string",
"description": "a shot already filmed: kept, and not paid for again"
},
"action": {
"type": "string",
"description": "the one thing that physically happens, in plain words"
},
"matchNext": {
"type": "boolean",
"description": "the shot ends on the next scene's picture"
},
"stillPrompt": {
"type": "string",
"description": "a picture to paint first, for a scene with none"
}
}
},
"description": "per scene, what happens in its picture; omit and the planner writes them"
},
"music": {
"type": "string",
"description": "'off' = silent"
},
"theme": {
"enum": [
"light",
"dark"
],
"type": "string",
"description": "logo: dark for a light logo"
},
"energy": {
"type": "number",
"description": "how much the film keeps moving, 0 to 1; 0 films exactly what is written"
},
"images": {
"type": "array",
"items": {
"type": "string"
},
"description": "pictures, hero first (https, upload_file URL or Library id)"
},
"moving": {
"type": "boolean",
"description": "true: one thing physically happens in each picture, filmed by a video model. false: stills moved by a virtual camera, far cheaper. Default true for launch, product, social"
},
"recipe": {
"enum": [
"type",
"logo",
"launch",
"product",
"social"
],
"type": "string",
"description": "type, logo, launch, product or social; help:true lists the recipes"
},
"confirm": {
"type": "boolean",
"description": "true renders the film for real credits; without it you get a free quote"
},
"preview": {
"type": "boolean",
"description": "true renders a 1-credit contact sheet to approve before the full render"
},
"seconds": {
"type": "number",
"description": "logo 4-10, others 6-60"
},
"tagline": {
"type": "string",
"description": "line shown under the headline or logo"
},
"projectUrl": {
"type": "string",
"description": "a past film's url; returns its saved spec to edit and render again"
},
"resolution": {
"type": "string",
"description": "the moving shots: 720p or 1080p (default) where the model has them"
}
}
}| format | string | renderer lane: 'explainer' (default; exact length) or 'faceless_channel' (2K, whole 10s blocks) |
| jobIds | array | SCHEDULE MODE: the episode job ids to schedule (finished ones are booked) |
| moreOf | any | MORE EPISODES: the `plan` an earlier call returned. Keeps its style and length, writes new episodes only |
| confirm | boolean | REQUIRED true to start rendering (real spend). Without it you get a free quote |
| captions | boolean | burn subtitles of every spoken word. Default: on only when the reference itself burns its spoken words as subtitles, otherwise off; nothing else is ever put on screen (no app screens, URLs, cards or stickers) unless the user asks |
| episodes | number | how many episodes, 1-10 (default 1) |
| planOnly | boolean | write the episode plan and style card, render nothing (a small planning charge) |
| schedule | object | SCHEDULE MODE: where and when the first episode posts; the rest follow every `everyHours` (default 24) |
| reference | string | a video link whose style every episode copies |
| aspectRatio | string | default: the reference's shape, else 9:16 |
| postCaptions | array | SCHEDULE MODE: one post caption per episode, in order (default: the episode title) |
| episodeTitles | array | your own episode titles, in order (sets the episode count) |
| durationSeconds | number | length of EACH episode, 20-120 seconds (default 30) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"plan": {
"description": "the `plan` a planOnly call returned, to run it without planning again"
},
"style": {
"type": "string",
"description": "override the look in words (e.g. \"flat 2D cartoon\") instead of the reference's"
},
"topic": {
"type": "string",
"description": "what the series is about, in the user's words"
},
"voice": {
"type": "string",
"description": "narrator: 'female' or 'male' (default follows the reference), or any narration voice name make_explainer takes"
},
"format": {
"enum": [
"explainer",
"faceless_channel"
],
"type": "string",
"description": "renderer lane: 'explainer' (default; exact length) or 'faceless_channel' (2K, whole 10s blocks)"
},
"jobIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "SCHEDULE MODE: the episode job ids to schedule (finished ones are booked)"
},
"moreOf": {
"description": "MORE EPISODES: the `plan` an earlier call returned. Keeps its style and length, writes new episodes only"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to start rendering (real spend). Without it you get a free quote"
},
"captions": {
"type": "boolean",
"description": "burn subtitles of every spoken word. Default: on only when the reference itself burns its spoken words as subtitles, otherwise off; nothing else is ever put on screen (no app screens, URLs, cards or stickers) unless the user asks"
},
"episodes": {
"type": "number",
"description": "how many episodes, 1-10 (default 1)"
},
"planOnly": {
"type": "boolean",
"description": "write the episode plan and style card, render nothing (a small planning charge)"
},
"schedule": {
"type": "object",
"required": [
"channels",
"at"
],
"properties": {
"at": {
"type": "string",
"description": "time of the first post: YYYY-MM-DDTHH:MM wall-clock, or an ISO date-time"
},
"channels": {
"type": "array",
"items": {
"type": "string"
},
"description": "channels to post each episode to, e.g. tiktok, instagram, youtube (as schedule_post takes them)"
},
"timezone": {
"type": "string",
"description": "timezone name for at, as schedule_post takes it"
},
"everyHours": {
"type": "number",
"description": "hours between episodes; default 24"
}
},
"description": "SCHEDULE MODE: where and when the first episode posts; the rest follow every `everyHours` (default 24)"
},
"reference": {
"type": "string",
"description": "a video link whose style every episode copies"
},
"aspectRatio": {
"enum": [
"9:16",
"16:9",
"1:1"
],
"type": "string",
"description": "default: the reference's shape, else 9:16"
},
"postCaptions": {
"type": "array",
"items": {
"type": "string"
},
"description": "SCHEDULE MODE: one post caption per episode, in order (default: the episode title)"
},
"episodeTitles": {
"type": "array",
"items": {
"type": "string"
},
"description": "your own episode titles, in order (sets the episode count)"
},
"durationSeconds": {
"type": "number",
"description": "length of EACH episode, 20-120 seconds (default 30)"
}
}
}| count | integer | ads per product, 1-20 (default 4) |
| model | string | image model id from hermoso_capabilities; omit for the default |
| angles | array | the selling angles to spread across, e.g. ['price or offer', 'material story', 'how it is used', 'lifestyle moment']; omit and the planner picks a spread |
| dryRun | boolean | true = plan and quote only, nothing rendered |
| batchId | string | the batchId a previous call returned, so the rest lands in the same batch |
| brandId | string | a profile id/name from list_brands; omit for the active profile (passing it pins it like use_brand) |
| layouts | array | your own layout descriptions in words, used slot by slot instead of the built-in layout set |
| language | string | language of the on-image copy (default English) |
| products | array | product ids or names from list_products, ['all'] for every product with a photo, or omit for the hero product |
| aspectRatios | array | canvases; every ad is rendered once per ratio (default ['4:5']) |
| logoPlacement | string | overlay (default) | in_scene (real file printed onto a blank surface) | none | auto |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"plan": {
"type": "array",
"items": {},
"description": "the `plan` (or `remaining`) a previous call returned: render exactly those ads, no new planning"
},
"brief": {
"type": "string",
"description": "what to lean into: a sale the brand is really running, a season, an audience, a tone"
},
"count": {
"type": "integer",
"maximum": 20,
"minimum": 1,
"description": "ads per product, 1-20 (default 4)"
},
"model": {
"type": "string",
"description": "image model id from hermoso_capabilities; omit for the default"
},
"angles": {
"type": "array",
"items": {
"type": "string"
},
"description": "the selling angles to spread across, e.g. ['price or offer', 'material story', 'how it is used', 'lifestyle moment']; omit and the planner picks a spread"
},
"dryRun": {
"type": "boolean",
"description": "true = plan and quote only, nothing rendered"
},
"batchId": {
"type": "string",
"description": "the batchId a previous call returned, so the rest lands in the same batch"
},
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands; omit for the active profile (passing it pins it like use_brand)"
},
"layouts": {
"type": "array",
"items": {
"type": "string"
},
"description": "your own layout descriptions in words, used slot by slot instead of the built-in layout set"
},
"language": {
"type": "string",
"description": "language of the on-image copy (default English)"
},
"products": {
"type": "array",
"items": {
"type": "string"
},
"description": "product ids or names from list_products, ['all'] for every product with a photo, or omit for the hero product"
},
"aspectRatios": {
"type": "array",
"items": {
"enum": [
"1:1",
"4:5",
"9:16"
],
"type": "string"
},
"description": "canvases; every ad is rendered once per ratio (default ['4:5'])"
},
"logoPlacement": {
"enum": [
"auto",
"overlay",
"in_scene",
"none"
],
"type": "string",
"description": "overlay (default) | in_scene (real file printed onto a blank surface) | none | auto"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"config"
],
"properties": {
"config": {
"type": "object",
"properties": {},
"description": "MUST include config.template: 'custom' or a preset id, plus its fields; language ('Portuguese') for its own words. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4, which takes config.music like any video) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? } is OPTIONAL on every video preset: include it ONLY when the user asked for an end card (a closing card with their logo); without it the video ends on its own last frame. palette and fontStack optional.",
"additionalProperties": {}
}
}
}| string |
| a brand logo URL or path to place into the composition |
| split | object | split/panel LAYOUT — only when the user asks for one ("split", "before/after", "versus screen"). "X vs Y" as a SCENE stays one unified frame |
| takes | number | camera takes per emotion, 1–4: designed framing / low-angle hero / extreme close-up / wide dutch tilt |
| topic | string | the video's topic — used to pick the hero object when you don't name keyElements |
| tweak | object | surgical pixel-faithful edit of a FINISHED thumbnail (needs sourceImage): kind emotion / background / background_color / rim_light, or any other kind with the edit in words as value |
| logo3d | boolean | first turn the flat logo into a volumetric 3D render (one extra billed image), then composite that |
| people | array | people described in prose instead of by photo (each still gets the chosen expression) |
| emotion | string | the expression on the face (default 'shock') — shock · hype · fear · confusion · determination · smug · charisma · disgust · awe · rage · laugh, or your own phrase |
| bakeText | boolean | default false. true paints the headline INTO the generation — only on an explicit user ask; it leaks garbled text elsewhere in the frame |
| emotions | array | render one variant per emotion (variants = emotions × takes, max 16) |
| headline | string | 2–4 word headline. Typeset OVER the finished render by default (free, always legible); newlines split it into stacked lines |
| location | string | place, time of day, weather, atmosphere |
| rimColor | string | colored back+hair light — ONLY when the user names one: 'ice-blue' / 'neon-magenta' / 'toxic-lime' / 'amber-gold' / 'pure-white' |
| variants | number | how many thumbnails to render (default 1, max 16). Each is its own billed render — offer a set of 4 rather than assuming |
| framework | string | concept framework id (default 'posed_portrait') — before_after · social_ui · three_step · screenshot · posed_portrait · posed_action · specific_day · graphical · landscape · map_aerial · product · adding_text · repetition · size_difference · news_clip · amplified_reality — or your own concept in words |
| reference | object | fields YOU extracted by eye from a reference thumbnail. Extract ALL of: brief (one dense sentence on the concept), subject (pose/action generically, NEVER a specific identity), elements, location, composition, background, split (boolean), split_count, person_count (0-3), emotion (one of the 11 presets or 'other'), emotion_detail (one vivid sentence covering eyes, brows, mouth, head angle). emotion + emotion_detail carry the reference's actual facial performance, which is the single biggest CTR lever on a face; split/split_count reproduce its panel structure. The reference image itself is never sent to the model |
| background | string | override the default bold saturated colour-field background |
| faceImages | array | up to 3 face photos (URLs or local paths) — each becomes a locked CHARACTER identity, in order |
| sceneBrief | string | what the thumbnail depicts — the concept in one dense sentence, rendered exactly |
| aspectRatio | string | '16:9' (YouTube, default) / '9:16' (Shorts) / '4:5' (Instagram) / '4:3' / '1:1' |
| bakedUiText | string | short label for a text-carrying framework (a chat bubble, a DAY N badge, a news lower-third, a map callout) — needs frameworkRequested:true |
| composition | string | override the default large-foreground-subject composition |
| keyElements | string | signature props / effects that make it pop — oversized, flying toward camera |
| sourceImage | string | the finished thumbnail URL a `tweak` edits; tweaks chain, so feed each accepted output into the next |
| overlayStyle | string | headline style: beast (default), fire, neon-lime, clean-glass, marker, or your own CSS declarations |
| forceGenerate | boolean | render the 'screenshot' framework anyway (it is normally a real video frame, not a generation) |
| headlineLines | array | explicit headline lines (up to 3) — overrides splitting `headline` on newlines |
| headlinePlace | string | bottom (default), top, center, or a 0-1 fraction from the top; never over the face |
| logoPlacement | string | overlay (exact, flat) | in_scene (the real file printed onto a blank panel in the shot; with logo3d, a 3D render of it, checked); auto: in_scene with logo3d |
| restrainedGrade | boolean | true for a calm / premium / muted look instead of the default punchy poster grade |
| castGenericPerson | boolean | pass true only after the user has explicitly chosen a generated stranger over their own face |
| frameworkRequested | boolean | true ONLY when the USER named this framework — it is what authorizes a text-carrying framework (social_ui / news_clip / specific_day / map_aerial) to bake its short UI label |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"font": {
"type": "string",
"description": "headline font: Anton (default) or any Google Fonts family"
},
"logo": {
"type": "string",
"description": "a brand logo URL or path to place into the composition"
},
"split": {
"type": "object",
"required": [
"mode"
],
"properties": {
"mode": {
"enum": [
"plain",
"before_after",
"versus",
"custom"
],
"type": "string",
"description": "layout: plain, before_after, versus or custom"
},
"panels": {
"type": "array",
"items": {
"type": "string"
},
"description": "panel descriptions in order, left to right or top to bottom; sets the panel count (minimum 2)"
}
},
"description": "split/panel LAYOUT — only when the user asks for one (\"split\", \"before/after\", \"versus screen\"). \"X vs Y\" as a SCENE stays one unified frame",
"additionalProperties": {}
},
"takes": {
"type": "number",
"description": "camera takes per emotion, 1–4: designed framing / low-angle hero / extreme close-up / wide dutch tilt"
},
"topic": {
"type": "string",
"description": "the video's topic — used to pick the hero object when you don't name keyElements"
},
"tweak": {
"type": "object",
"required": [
"kind",
"value"
],
"properties": {
"kind": {
"type": "string",
"description": "emotion, background, background_color or rim_light, or another kind with the edit in words"
},
"value": {
"type": "string",
"description": "new value for the kind: an emotion, a colour, or the edit in words"
}
},
"description": "surgical pixel-faithful edit of a FINISHED thumbnail (needs sourceImage): kind emotion / background / background_color / rim_light, or any other kind with the edit in words as value"
},
"logo3d": {
"type": "boolean",
"description": "first turn the flat logo into a volumetric 3D render (one extra billed image), then composite that"
},
"people": {
"type": "array",
"items": {
"type": "object",
"required": [
"describe"
],
"properties": {
"describe": {
"type": "string",
"description": "the person's appearance in English prose, used instead of a face photo"
}
},
"additionalProperties": {}
},
"description": "people described in prose instead of by photo (each still gets the chosen expression)"
},
"emotion": {
"type": "string",
"description": "the expression on the face (default 'shock') — shock · hype · fear · confusion · determination · smug · charisma · disgust · awe · rage · laugh, or your own phrase"
},
"bakeText": {
"type": "boolean",
"description": "default false. true paints the headline INTO the generation — only on an explicit user ask; it leaks garbled text elsewhere in the frame"
},
"emotions": {
"type": "array",
"items": {
"type": "string"
},
"description": "render one variant per emotion (variants = emotions × takes, max 16)"
},
"headline": {
"type": "string",
"description": "2–4 word headline. Typeset OVER the finished render by default (free, always legible); newlines split it into stacked lines"
},
"location": {
"type": "string",
"description": "place, time of day, weather, atmosphere"
},
"rimColor": {
"type": "string",
"description": "colored back+hair light — ONLY when the user names one: 'ice-blue' / 'neon-magenta' / 'toxic-lime' / 'amber-gold' / 'pure-white'"
},
"variants": {
"type": "number",
"description": "how many thumbnails to render (default 1, max 16). Each is its own billed render — offer a set of 4 rather than assuming"
},
"framework": {
"type": "string",
"description": "concept framework id (default 'posed_portrait') — before_after · social_ui · three_step · screenshot · posed_portrait · posed_action · specific_day · graphical · landscape · map_aerial · product · adding_text · repetition · size_difference · news_clip · amplified_reality — or your own concept in words"
},
"reference": {
"type": "object",
"properties": {},
"description": "fields YOU extracted by eye from a reference thumbnail. Extract ALL of: brief (one dense sentence on the concept), subject (pose/action generically, NEVER a specific identity), elements, location, composition, background, split (boolean), split_count, person_count (0-3), emotion (one of the 11 presets or 'other'), emotion_detail (one vivid sentence covering eyes, brows, mouth, head angle). emotion + emotion_detail carry the reference's actual facial performance, which is the single biggest CTR lever on a face; split/split_count reproduce its panel structure. The reference image itself is never sent to the model",
"additionalProperties": {}
},
"background": {
"type": "string",
"description": "override the default bold saturated colour-field background"
},
"faceImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 3 face photos (URLs or local paths) — each becomes a locked CHARACTER identity, in order"
},
"sceneBrief": {
"type": "string",
"description": "what the thumbnail depicts — the concept in one dense sentence, rendered exactly"
},
"aspectRatio": {
"type": "string",
"description": "'16:9' (YouTube, default) / '9:16' (Shorts) / '4:5' (Instagram) / '4:3' / '1:1'"
},
"bakedUiText": {
"type": "string",
"description": "short label for a text-carrying framework (a chat bubble, a DAY N badge, a news lower-third, a map callout) — needs frameworkRequested:true"
},
"composition": {
"type": "string",
"description": "override the default large-foreground-subject composition"
},
"keyElements": {
"type": "string",
"description": "signature props / effects that make it pop — oversized, flying toward camera"
},
"sourceImage": {
"type": "string",
"description": "the finished thumbnail URL a `tweak` edits; tweaks chain, so feed each accepted output into the next"
},
"overlayStyle": {
"type": "string",
"description": "headline style: beast (default), fire, neon-lime, clean-glass, marker, or your own CSS declarations"
},
"forceGenerate": {
"type": "boolean",
"description": "render the 'screenshot' framework anyway (it is normally a real video frame, not a generation)"
},
"headlineLines": {
"type": "array",
"items": {
"type": "string"
},
"description": "explicit headline lines (up to 3) — overrides splitting `headline` on newlines"
},
"headlinePlace": {
"type": "string",
"description": "bottom (default), top, center, or a 0-1 fraction from the top; never over the face"
},
"logoPlacement": {
"enum": [
"auto",
"overlay",
"in_scene"
],
"type": "string",
"description": "overlay (exact, flat) | in_scene (the real file printed onto a blank panel in the shot; with logo3d, a 3D render of it, checked); auto: in_scene with logo3d"
},
"restrainedGrade": {
"type": "boolean",
"description": "true for a calm / premium / muted look instead of the default punchy poster grade"
},
"castGenericPerson": {
"type": "boolean",
"description": "pass true only after the user has explicitly chosen a generated stranger over their own face"
},
"frameworkRequested": {
"type": "boolean",
"description": "true ONLY when the USER named this framework — it is what authorizes a text-carrying framework (social_ui / news_clip / specific_day / map_aerial) to bake its short UI label"
}
}
}| the filled-cell count an unconfirmed delete reported |
| spreadsheetId | string | Google spreadsheet id; use this or sheetUrl |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"action"
],
"properties": {
"tab": {
"type": "string",
"description": "which tab — its title or numeric sheetId (rename / delete)"
},
"title": {
"type": "string",
"description": "the name for the new tab (action:\"add\")"
},
"action": {
"enum": [
"add",
"rename",
"delete"
],
"type": "string",
"description": "add, rename or delete a tab"
},
"confirm": {
"type": "boolean",
"description": "true deletes a tab that holds data; needs confirmCells"
},
"newTitle": {
"type": "string",
"description": "what to rename the tab to (action:\"rename\")"
},
"sheetUrl": {
"type": "string",
"description": "Google Sheets link, instead of spreadsheetId"
},
"confirmCells": {
"type": "number",
"description": "the filled-cell count an unconfirmed delete reported"
},
"spreadsheetId": {
"type": "string",
"description": "Google spreadsheet id; use this or sheetUrl"
}
}
}| useOwnReviewsOnly | boolean | true = mine only your reviews, no public search (no search credits). Default false = merge with public customer language. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands to mine for; omit to use the active profile"
},
"reviews": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "your own customer reviews: a list of review texts, or one pasted block (one per line, numbered, blank-line separated, or CSV with a review column)"
},
"reviewsUrl": {
"type": "string",
"description": "a URL of your reviews: an uploaded CSV, TXT or JSON file (from upload_file) or a review page. On a local CLI a file path also works."
},
"useOwnReviewsOnly": {
"type": "boolean",
"description": "true = mine only your reviews, no public search (no search credits). Default false = merge with public customer language."
}
}
}| plan | any | the `plan` a dryRun returned, rendered without planning again |
| count | number | how many variants, 1-12 (default 6) |
| notes | string | anything the variants must respect, e.g. "keep it women 25-40", "no gyms" |
| video* | string | the source video: a file URL, or a TikTok, Reel, Facebook, X or YouTube post link |
| change | string | what should change, in the user's own words, e.g. 'older women, winter streets'; every variant applies it (omit to let the variants vary freely) |
| dryRun | boolean | true = the paid plan and the price, no render |
| images | array | up to 9 pictures to put in the variants, in the order @Image1, @Image2 name them: URLs or local paths |
| quality | string | 1080p is the default; 720p costs less, and hands holding a product are less reliable at 720p |
| regions | array | markets to restyle for, one or more variants each, e.g. ["Berlin","Tokyo","São Paulo"] — visuals only; audio is never translated here |
| useBrand | boolean | false = send no brand product photo (for a video that is not this workspace brand) |
| quoteOnly | boolean | true = the exact credits, free; nothing runs |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video"
],
"properties": {
"axes": {
"type": "array",
"items": {
"type": "string"
},
"description": "what to vary: character, outfit, location, objects (default all four), or your own, e.g. \"season\""
},
"plan": {
"description": "the `plan` a dryRun returned, rendered without planning again"
},
"count": {
"type": "number",
"description": "how many variants, 1-12 (default 6)"
},
"notes": {
"type": "string",
"description": "anything the variants must respect, e.g. \"keep it women 25-40\", \"no gyms\""
},
"video": {
"type": "string",
"description": "the source video: a file URL, or a TikTok, Reel, Facebook, X or YouTube post link"
},
"change": {
"type": "string",
"description": "what should change, in the user's own words, e.g. 'older women, winter streets'; every variant applies it (omit to let the variants vary freely)"
},
"dryRun": {
"type": "boolean",
"description": "true = the paid plan and the price, no render"
},
"images": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"type": "string"
},
"role": {
"type": "string",
"description": "what it is, in your words, e.g. \"our founder\""
}
}
}
]
},
"description": "up to 9 pictures to put in the variants, in the order @Image1, @Image2 name them: URLs or local paths"
},
"quality": {
"enum": [
"1080p",
"720p"
],
"type": "string",
"description": "1080p is the default; 720p costs less, and hands holding a product are less reliable at 720p"
},
"regions": {
"type": "array",
"items": {
"type": "string"
},
"description": "markets to restyle for, one or more variants each, e.g. [\"Berlin\",\"Tokyo\",\"São Paulo\"] — visuals only; audio is never translated here"
},
"useBrand": {
"type": "boolean",
"description": "false = send no brand product photo (for a video that is not this workspace brand)"
},
"quoteOnly": {
"type": "boolean",
"description": "true = the exact credits, free; nothing runs"
}
}
}| string |
| 'image', 'video', or 'auto' when unspecified |
| recipe | string | a recipe id from hermoso_capabilities to force an archetype |
| talent | string | who is on camera; omit to follow the format. Say 'someone new' in product to skip reusing a saved creator. |
| inserts | array | VIDEO ONLY: the user's own SCREENS cut in at these seconds (each item's look, kind, start and end say how), the voice running underneath. How many: at most 8, about one per 2.5 s of film (6 s: 2, 10 s: 4, 15 s: 6, 20 s: 8, 30 s: 8), 1.2-2 s each, on the line that talks about it. When there are more, the most crowded are left out first, never the last (the payoff) or one marked key, and the reply names them. The plan keeps those shots free of screens and returns the pictures as creative.inserts for render_ad to lay in (about 2 credits). One that does not fit is left out and the reply says so. |
| product* | string | what to advertise + any angle/offer the user specified |
| setting | string | force the WHERE — a setting id from list_hooks (e.g. "kitchen", "gym", or a surreal one like "volcano_rim" / "airplane_wing", which are played 100% straight and never acknowledged). Omit for a neutral setting. |
| concepts | array | several variants: one line per concept (a different angle each); built exactly |
| language | string | output language for the ad copy (e.g. Spanish) — default English |
| reference | string | a reference to clone: an ad-library link (Facebook Ad Library, LinkedIn Ad Library, Google Ads Transparency — its real copy/advertiser are fetched) OR a VIDEO link — a TikTok, Instagram Reel, Facebook video, X post, YouTube Short/video or a direct video file — which is WATCHED first (frames + voiceover/on-screen-text transcript) so the concept keeps its hook, structure and pacing. To remake one video for this brand at its own length, clone_video is the direct tool |
| durationSeconds | number | For video: the total length the user asked for, in seconds (30 for "a 30 second ad"). The storyboard’s scenes add up to it and the script is sized for it. Supported range 4 to 180 seconds; a value outside the range is clamped and the reply says so. A length that fits one clip of the render model renders as one continuous clip; a longer one is joined from clips filled to that model’s clip maximum, remainder last (on a 15s-clip model, 40 becomes 15+15+10 and 17 becomes 13+4), and is never sped up. The clip maximum depends on the model: 15s on most, 30s on the longest-clip model, so a 30s ad can be one take. Leave it out when the user named no length; the planner then picks one that suits the format. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"product"
],
"properties": {
"hook": {
"type": "string",
"description": "force the VISUAL scroll-stop mechanic the opening beat is built on — a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"macro_asmr\"). Omit to let the planner pick. A hook that cannot be delivered in this brief is DROPPED with the reason rather than rendered wrongly — an on-screen-text hook on an authentic/UGC ad is the one that bites, because that register carries zero on-screen text."
},
"brand": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {},
"additionalProperties": {}
}
],
"description": "brand name, or a brand profile object {name,domain,category,palette,products,…}. OMIT to use the workspace’s SAVED brand + memory automatically (see get_brand); use draft_brand to onboard a new one"
},
"draft": {
"type": "object",
"required": [
"model",
"durationSeconds"
],
"properties": {
"model": {
"type": "string",
"description": "video model id from a refusal that offered a lighter draft; use only the value it gave"
},
"durationSeconds": {
"type": "number",
"description": "clip length in seconds from that same refusal; use only the value it gave"
}
},
"description": "Only after a video refusal that offered a lighter draft: the {model, durationSeconds} that refusal named. The plan is then written to that length and priced on that model. Use only the values the refusal gave; when an account cannot cover a video, the refusal comes before planning with three options (an image, adding credits, or this draft when one fits) and the user chooses."
},
"format": {
"enum": [
"auto",
"image",
"video"
],
"type": "string",
"description": "'image', 'video', or 'auto' when unspecified"
},
"recipe": {
"type": "string",
"description": "a recipe id from hermoso_capabilities to force an archetype"
},
"talent": {
"enum": [
"auto",
"creator",
"product_only"
],
"type": "string",
"description": "who is on camera; omit to follow the format. Say 'someone new' in product to skip reusing a saved creator."
},
"inserts": {
"type": "array",
"items": {
"type": "object",
"required": [
"image",
"start",
"end"
],
"properties": {
"end": {
"type": "number",
"description": "second it ends: at most 2 s after start (2.5 s full-screen), never in the last 1.5 s of the film"
},
"key": {
"type": "boolean",
"description": "true = the story needs this one (the payoff): never left out when there are more screens than the length takes"
},
"kind": {
"enum": [
"screen",
"photo"
],
"type": "string",
"description": "screen (default): an app, page, dashboard, document, review or chart. photo: a person, product or place, NOT laid over the video (a product photo goes in as the brand's product photo, a person as a saved creator)"
},
"look": {
"enum": [
"cutaway",
"card",
"full"
],
"type": "string",
"description": "cutaway (default): a hard cut to the screen filling the frame, cropped to focus, held still, never enlarged past 1.25x its pixels (any gap is the screenshot's own background colour; a screen too small to read is left out and said). card: picture-in-picture over the playing footage, clear of the speaker's face. full: the whole picture uncropped on a blurred backdrop. card and full ONLY when the user asked"
},
"focus": {
"type": "object",
"required": [
"x",
"y",
"w",
"h"
],
"properties": {
"h": {
"type": "number"
},
"w": {
"type": "number"
},
"x": {
"type": "number"
},
"y": {
"type": "number"
}
},
"description": "the readable part to fill the frame with, as fractions of the picture from its top-left (e.g. the flagged-items table); a crop as tall as wide, or taller, reads best on a phone. Omit for the whole picture, plain margins trimmed"
},
"image": {
"type": "string",
"description": "an https or upload_file URL, a Library id (L0, L1…) or a local path on the CLI"
},
"start": {
"type": "number",
"description": "second it appears, never before 1 s (the film opens on moving footage)"
}
}
},
"maxItems": 8,
"description": "VIDEO ONLY: the user's own SCREENS cut in at these seconds (each item's look, kind, start and end say how), the voice running underneath. How many: at most 8, about one per 2.5 s of film (6 s: 2, 10 s: 4, 15 s: 6, 20 s: 8, 30 s: 8), 1.2-2 s each, on the line that talks about it. When there are more, the most crowded are left out first, never the last (the payoff) or one marked key, and the reply names them. The plan keeps those shots free of screens and returns the pictures as creative.inserts for render_ad to lay in (about 2 credits). One that does not fit is left out and the reply says so."
},
"product": {
"type": "string",
"description": "what to advertise + any angle/offer the user specified"
},
"setting": {
"type": "string",
"description": "force the WHERE — a setting id from list_hooks (e.g. \"kitchen\", \"gym\", or a surreal one like \"volcano_rim\" / \"airplane_wing\", which are played 100% straight and never acknowledged). Omit for a neutral setting."
},
"concepts": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 4,
"description": "several variants: one line per concept (a different angle each); built exactly"
},
"language": {
"type": "string",
"description": "output language for the ad copy (e.g. Spanish) — default English"
},
"reference": {
"type": "string",
"description": "a reference to clone: an ad-library link (Facebook Ad Library, LinkedIn Ad Library, Google Ads Transparency — its real copy/advertiser are fetched) OR a VIDEO link — a TikTok, Instagram Reel, Facebook video, X post, YouTube Short/video or a direct video file — which is WATCHED first (frames + voiceover/on-screen-text transcript) so the concept keeps its hook, structure and pacing. To remake one video for this brand at its own length, clone_video is the direct tool"
},
"durationSeconds": {
"type": "number",
"description": "For video: the total length the user asked for, in seconds (30 for \"a 30 second ad\"). The storyboard’s scenes add up to it and the script is sized for it. Supported range 4 to 180 seconds; a value outside the range is clamped and the reply says so. A length that fits one clip of the render model renders as one continuous clip; a longer one is joined from clips filled to that model’s clip maximum, remainder last (on a 15s-clip model, 40 becomes 15+15+10 and 17 becomes 13+4), and is never sped up. The clip maximum depends on the model: 15s on most, 30s on the longest-clip model, so a 30s ad can be one take. Leave it out when the user named no length; the planner then picks one that suits the format."
}
}
}| ops* | array | the ordered edit plan (max 6 ops) |
| accent | string | override the brand accent hex |
| domain | string | override the brand website |
| dryRun | boolean | true = return the estimated credits this edit reserves and run nothing |
| videoUrl* | string | the video to edit: a render / Library URL, a direct file, or a public post link; a URL, not a local file path; use upload_file for local files |
| brandName | string | override the workspace brand name |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"videoUrl",
"ops"
],
"properties": {
"ops": {
"type": "array",
"items": {
"type": "object",
"required": [
"op"
],
"properties": {
"x": {
"type": "number",
"description": "text/watermark centre: 0-1 of frame, or px"
},
"y": {
"type": "number",
"description": "vertical centre for text or watermark: 0 to 1 of frame height, or pixels"
},
"at": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"word"
],
"properties": {
"word": {
"type": "string",
"description": "the word or phrase as it is said"
},
"occurrence": {
"type": "number",
"description": "1 = first time said (default), 2 = second, -1 = last"
}
}
}
],
"description": "text, instead of start/end: the words appear as this spoken phrase starts (timed from the video's own words; refused if it is never said)"
},
"db": {
"type": "number",
"description": "audio_gain -20..+6 dB / music: trim on its automatic level, -20..+12"
},
"op": {
"enum": [
"trim",
"speed",
"mute",
"audio_gain",
"fade_out",
"music",
"voiceover",
"append_card",
"watermark",
"grain",
"text",
"join"
],
"type": "string",
"description": "the edit to apply, in order; each op takes only the fields listed for it in the tool description"
},
"end": {
"type": "number",
"description": "trim/mute/text window end (s)"
},
"sub": {
"type": "string",
"description": "append_card: pill line (default: website) / text: a second, smaller line"
},
"duck": {
"anyOf": [
{
"type": "string",
"const": "auto"
},
{
"type": "boolean"
}
],
"description": "music: 'auto' default = dips while the clip's own sound plays"
},
"from": {
"type": "number",
"description": "music: the second of the track to start from"
},
"mood": {
"type": "string",
"description": "music: mood words for the library pick, or the brief for generated music"
},
"text": {
"type": "string",
"description": "text/voiceover: the words, verbatim"
},
"clips": {
"type": "array",
"items": {
"type": "object",
"required": [
"url"
],
"properties": {
"end": {
"type": "number",
"description": "seconds into this clip to stop at"
},
"url": {
"type": "string",
"description": "clip to join after the video: Library URL, direct file or public post link"
},
"match": {
"description": "seam match for this clip: auto (default), off, or a grade object"
},
"start": {
"type": "number",
"description": "seconds into this clip to start from"
},
"reframe": {
"description": "shot change at this seam: auto, off, or a step from 1.1 to 1.5"
}
}
},
"description": "join: the clips after this video"
},
"match": {
"description": "join: seam match, 'auto' default | 'off' | {grade,level,grain,blur,strength}"
},
"model": {
"type": "string",
"description": "music, generate: the model"
},
"start": {
"type": "number",
"description": "trim/mute/text window start (s)"
},
"style": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {},
"additionalProperties": {}
}
],
"description": "text: a look name or a textStyle"
},
"track": {
"type": "string",
"description": "music: omit = a library track by mood; a library track name; an audio link; 'generate' (paid, only on the user's ask)"
},
"until": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"word"
],
"properties": {
"word": {
"type": "string",
"description": "the word or phrase as it is said"
},
"occurrence": {
"type": "number",
"description": "counted from the at phrase on"
}
}
}
],
"description": "text with at: the words leave as this phrase ends"
},
"voice": {
"type": "string",
"description": "voiceover voice: female or male"
},
"bridge": {
"type": "object",
"required": [
"kind"
],
"properties": {
"kind": {
"enum": [
"impact",
"text"
],
"type": "string",
"description": "impact (cut before the hook's payoff) or text (words over the cut)"
},
"text": {
"type": "string",
"description": "text bridge only: words shown over the cut, only when the user asks for them"
},
"then": {
"type": "string",
"description": "text bridge only: a line shown after the text"
},
"cutAt": {
"type": "number",
"description": "omit: found from the footage"
},
"flash": {
"type": "boolean",
"description": "true (default) adds a white flash on the cut; false skips it"
},
"shake": {
"type": "boolean",
"description": "true (default) adds a camera shake on the cut; false skips it"
},
"sound": {
"type": "string",
"description": "'auto' default, 'own', 'impact', 'whoosh', 'none', or an audio URL (find_sound)"
},
"matchCut": {
"type": "number",
"description": "second in our clip where it starts"
}
},
"description": "join: connects the hook to the first clip"
},
"corner": {
"enum": [
"tl",
"tr",
"bl",
"br"
],
"type": "string",
"description": "watermark corner (default br), or x/y"
},
"factor": {
"type": "number",
"description": "speed 0.5-2"
},
"fadeOut": {
"type": "number",
"description": "music: fade-out seconds 0-5"
},
"reframe": {
"description": "join: shot change at a same-framing stitch, 'auto' | 'off' | step 1.1-1.5"
},
"replace": {
"type": "boolean",
"description": "music: true = replaces the clip's own sound"
},
"seconds": {
"type": "number",
"description": "fade_out 0.3-3s / append_card 2-5s / transition 0.2-1.5s"
},
"tagline": {
"type": "string",
"description": "append_card: smaller line under the headline"
},
"duration": {
"type": "number",
"description": "text with at and no until: seconds the words stay, 0.3-30 (default 2)"
},
"headline": {
"type": "string",
"description": "append_card: big line (default: brand name)"
},
"position": {
"enum": [
"top",
"center",
"lower",
"bottom"
],
"type": "string",
"description": "text: where, or x/y"
},
"card_html": {
"type": "string",
"description": "append_card: your OWN full-frame card as inline-styled HTML ({{logo}} = the brand logo)"
},
"intensity": {
"anyOf": [
{
"enum": [
"default",
"strong"
],
"type": "string"
},
{
"type": "number"
}
],
"description": "grain: or 0-1 (default 0.25)"
},
"keepSound": {
"type": "boolean",
"description": "voiceover only: true (default) keeps the clip's sound under the voice; false replaces it"
},
"textStyle": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {},
"additionalProperties": {}
}
],
"description": "text op: a textStyle object or preset name, same as style"
},
"background": {
"type": "string",
"description": "append_card: hex or a colour name; the user's colour beats the brand palette"
},
"transition": {
"type": "string",
"description": "join: 'cut' default, 'crossfade', or any ffmpeg xfade name (wipeleft…)"
}
}
},
"description": "the ordered edit plan (max 6 ops)"
},
"accent": {
"type": "string",
"description": "override the brand accent hex"
},
"domain": {
"type": "string",
"description": "override the brand website"
},
"dryRun": {
"type": "boolean",
"description": "true = return the estimated credits this edit reserves and run nothing"
},
"videoUrl": {
"type": "string",
"description": "the video to edit: a render / Library URL, a direct file, or a public post link; a URL, not a local file path; use upload_file for local files"
},
"brandName": {
"type": "string",
"description": "override the workspace brand name"
}
}
}| string |
| restrict to one channel |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"axis": {
"enum": [
"hook",
"subject",
"recipe",
"channel",
"media",
"hour"
],
"type": "string",
"description": "what to group by — default hook; recipe = the format of the creative"
},
"days": {
"type": "number",
"description": "look back N days (1-730) over the whole history; omit for the recent posts only"
},
"brand": {
"type": "string",
"description": "WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done."
},
"channel": {
"type": "string",
"description": "restrict to one channel"
}
}
}| what the sizzle should show — the product, the setting, the look |
| seconds | number | finished length, clamped to 18-30s (default 25). The PAID hero render is always 15s regardless — this only changes how the cuts and cards are packed |
| refImage | string | product packshot URL that anchors the real label — strongly recommended |
| brandName | string | brand name on the cards — defaults to the workspace brand |
| musicMood | string | music-bed mood, e.g. driving / cinematic / upbeat |
| resolution | string | hero-clip resolution and therefore the whole cost — DEFAULT '1080p' (≈1,040 credits); '720p' ≈470, '480p' ≈220, '4k' ≈4,130 |
| aspectRatio | string | '9:16' default; anything the Seedance row that renders it does not list falls back to 9:16 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"prompt"
],
"properties": {
"cta": {
"type": "string",
"description": "closing CTA line, ≤30 chars"
},
"specs": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 4 spec lines for the typeset cards, ≤26 chars each"
},
"style": {
"enum": [
"hypermotion",
"product_2d",
"typography",
"dark_minimal",
"color_pop",
"mixed_media",
"saas",
"product_showcase"
],
"type": "string",
"description": "the LOOK of the sizzle (same price for every look): hypermotion (Fast cuts, crash zooms and speed ramps), product_2d (The real product with playful hand-drawn 2D animation around it), typography (Product framed by huge bold letterforms in the brand colours), dark_minimal (Moody, premium, a single hard light on black), color_pop (Bright saturated colour, glossy, joyful), mixed_media (Editorial collage: paper, cut-outs and texture around the product), saas (For apps and software: floating screens, light trails, particles), product_showcase (Hero commercial: ingredients, splashes and a clean pack shot). Default hypermotion."
},
"prompt": {
"type": "string",
"description": "what the sizzle should show — the product, the setting, the look"
},
"seconds": {
"type": "number",
"description": "finished length, clamped to 18-30s (default 25). The PAID hero render is always 15s regardless — this only changes how the cuts and cards are packed"
},
"refImage": {
"type": "string",
"description": "product packshot URL that anchors the real label — strongly recommended"
},
"brandName": {
"type": "string",
"description": "brand name on the cards — defaults to the workspace brand"
},
"musicMood": {
"type": "string",
"description": "music-bed mood, e.g. driving / cinematic / upbeat"
},
"resolution": {
"enum": [
"480p",
"720p",
"1080p",
"4k"
],
"type": "string",
"description": "hero-clip resolution and therefore the whole cost — DEFAULT '1080p' (≈1,040 credits); '720p' ≈470, '480p' ≈220, '4k' ≈4,130"
},
"aspectRatio": {
"type": "string",
"description": "'9:16' default; anything the Seedance row that renders it does not list falls back to 9:16"
}
}
}| companyName | string | the advertiser name, e.g. "Liquid Death" (company / brand / name are read as this too) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "same as domain"
},
"name": {
"type": "string",
"description": "same as companyName"
},
"sort": {
"type": "string",
"description": "'longest_running' (default) etc."
},
"brand": {
"type": "string",
"description": "same as companyName"
},
"limit": {
"type": "number",
"description": "max ads per platform (default 30)"
},
"domain": {
"type": "string",
"description": "the advertiser domain, e.g. liquiddeath.com — a full website URL works (url / website are read as this too). Pass companyName OR domain"
},
"company": {
"type": "string",
"description": "same as companyName"
},
"country": {
"type": "string",
"description": "2-letter, default 'US'"
},
"website": {
"type": "string",
"description": "same as domain"
},
"companyName": {
"type": "string",
"description": "the advertiser name, e.g. \"Liquid Death\" (company / brand / name are read as this too)"
}
}
}| string |
| ascending (default) or descending |
| databaseId | string | database id or link (list_notion_pages type:"database") |
| dataSourceId | string | data source id from an earlier read, as an alternative to databaseId |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1-100, default 50"
},
"match": {
"type": "object",
"description": "{\"Column\": value} — equality for status/select/checkbox/number/date, contains for text/multi-select",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"cursor": {
"type": "string",
"description": "nextCursor from the previous page of rows"
},
"filter": {
"type": "object",
"description": "a raw Notion filter object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"sortBy": {
"type": "string",
"description": "column name to sort by, or created_time or last_edited_time"
},
"archived": {
"type": "boolean",
"description": "read archived rows instead"
},
"direction": {
"enum": [
"ascending",
"descending"
],
"type": "string",
"description": "ascending (default) or descending"
},
"databaseId": {
"type": "string",
"description": "database id or link (list_notion_pages type:\"database\")"
},
"dataSourceId": {
"type": "string",
"description": "data source id from an earlier read, as an alternative to databaseId"
}
}
}| string |
| the viral hook: a post link or a video URL |
| mode | string | auto (always a swap), swap (edit what you describe), or motion (move your person in their own photo's setting) |
| swap | string | what changes, in your own words |
| tier | string | motion: the motion-transfer tier (default pro) |
| image | string | a picture of the subject instead: a product or a person. A person generate_image painted from words with no reference photo counts as an AI face on every plan |
| model | string | swap: omit for the default; a clip editor id (kling-4-edit, kling-o3-edit; hermoso_capabilities), or for one person swapped for another 'wan-3' (≤15 s) / 'minimax-h3-max-recast' |
| start | number | window start in seconds of the hook; minimum window is 3 seconds |
| voice | string | one person swapped in: a NEW voice for them instead of the original speaker's: 'match' (fits them), 'female'/'male', or Aria, Sarah, Charlotte, George, Brian, Charlie. Omit = keep the clip's voice. Never a cloned voice |
| outfit | string | what the subject (or, with no subject, the main person in the hook) wears instead: an outfit photo URL or words |
| prompt | string | extra direction for the render |
| confirm | boolean | true = render it (after the user okayed the quote) |
| creator | string | a saved creator's name or id (list_creators). NONE SAVED: cast a preset by exact name (list_creators), or pass image instead: a portrait of an AI person that generate_image painted from words (no reference photo) swaps in on every plan, nothing to save. A described lookalike of the user is fine on every plan; a photo of anyone is a real face (paid plan) |
| language | string | the new voice speaks this language (e.g. 'Spanish'), mouth lip-synced to the new words; voice defaults to 'match' |
| location | string | move the scene: a photo URL of the place, or words ("a sunny beach at golden hour") |
| subjects | array | a duo or group recast, 2-4 people, instead of creator/image |
| useBrand | boolean | false = no brand product photo; true with location/outfit and no subject = also swap in the brand product |
| faceRoute | string | 'face_lane' = the user's "I own the rights to this face" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own. |
| resolution | string | swap: the output tier, on an editor that prices by one (kling-4-edit 720p or 1080p); default the hook's own pixels |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"hook"
],
"properties": {
"end": {
"type": "number",
"description": "window end in seconds of the hook"
},
"hook": {
"type": "string",
"description": "the viral hook: a post link or a video URL"
},
"mode": {
"enum": [
"auto",
"swap",
"motion"
],
"type": "string",
"description": "auto (always a swap), swap (edit what you describe), or motion (move your person in their own photo's setting)"
},
"swap": {
"type": "string",
"description": "what changes, in your own words"
},
"tier": {
"enum": [
"pro",
"standard"
],
"type": "string",
"description": "motion: the motion-transfer tier (default pro)"
},
"image": {
"type": "string",
"description": "a picture of the subject instead: a product or a person. A person generate_image painted from words with no reference photo counts as an AI face on every plan"
},
"model": {
"type": "string",
"description": "swap: omit for the default; a clip editor id (kling-4-edit, kling-o3-edit; hermoso_capabilities), or for one person swapped for another 'wan-3' (≤15 s) / 'minimax-h3-max-recast'"
},
"start": {
"type": "number",
"description": "window start in seconds of the hook; minimum window is 3 seconds"
},
"voice": {
"type": "string",
"description": "one person swapped in: a NEW voice for them instead of the original speaker's: 'match' (fits them), 'female'/'male', or Aria, Sarah, Charlotte, George, Brian, Charlie. Omit = keep the clip's voice. Never a cloned voice"
},
"outfit": {
"type": "string",
"description": "what the subject (or, with no subject, the main person in the hook) wears instead: an outfit photo URL or words"
},
"prompt": {
"type": "string",
"description": "extra direction for the render"
},
"confirm": {
"type": "boolean",
"description": "true = render it (after the user okayed the quote)"
},
"creator": {
"type": "string",
"description": "a saved creator's name or id (list_creators). NONE SAVED: cast a preset by exact name (list_creators), or pass image instead: a portrait of an AI person that generate_image painted from words (no reference photo) swaps in on every plan, nothing to save. A described lookalike of the user is fine on every plan; a photo of anyone is a real face (paid plan)"
},
"language": {
"type": "string",
"description": "the new voice speaks this language (e.g. 'Spanish'), mouth lip-synced to the new words; voice defaults to 'match'"
},
"location": {
"type": "string",
"description": "move the scene: a photo URL of the place, or words (\"a sunny beach at golden hour\")"
},
"subjects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "optional label for this person"
},
"refs": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 3 more views of the SAME person (a saved creator's pose plates ride automatically)"
},
"image": {
"type": "string",
"description": "or a picture of this person"
},
"outfit": {
"type": "string",
"description": "what this person wears instead: an outfit photo URL or words; alone (no creator/image) it keeps this person and changes only the clothes"
},
"creator": {
"type": "string",
"description": "a saved creator's name or id"
},
"position": {
"type": "string",
"description": "where they are in the hook: 'left', 'right', 'center', or your own words ('who raps first'); all or none"
}
},
"additionalProperties": {}
},
"maxItems": 4,
"description": "a duo or group recast, 2-4 people, instead of creator/image"
},
"useBrand": {
"type": "boolean",
"description": "false = no brand product photo; true with location/outfit and no subject = also swap in the brand product"
},
"faceRoute": {
"enum": [
"face_lane"
],
"type": "string",
"description": "'face_lane' = the user's \"I own the rights to this face\" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own."
},
"resolution": {
"enum": [
"480p",
"720p",
"1080p"
],
"type": "string",
"description": "swap: the output tier, on an editor that prices by one (kling-4-edit 720p or 1080p); default the hook's own pixels"
}
}
}| voice |
| string |
| a NEW voice for the new performer: 'match' (a stock voice that fits them), 'female'/'male', or Aria, Sarah, Charlotte, George, Brian, Charlie. Omit = the clip's own sound. Never the original speaker's or a cloned voice |
| dryRun | boolean | return the credits this exact job would hold and the engine auto picks, without rendering |
| engine | string | default auto |
| images | array | seedance/wan: more pictures, @Image2…: a character, product, outfit or place |
| prompt | string | optional, e.g. 'same moves, new location: Tokyo at night' |
| language | string | the new voice speaks this language (e.g. 'Spanish'), lip-synced to the new words; voice defaults to 'match' |
| faceRoute | string | 'face_lane' = the user's "I own the rights to this face" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own. |
| resolution | string | a person swap defaults to 720p (seedance or kling-edit); otherwise seedance and wan default 1080p; h3 720p or 1080p, default the clip’s own |
| orientation | string | kling only: which aspect to keep: the video's (default) or the image's |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video"
],
"properties": {
"tier": {
"enum": [
"pro",
"standard"
],
"type": "string",
"description": "kling only: 'pro' (default): 1080p and real hand-object interaction. 'standard': about 25% fewer credits and faster, but 720p, and it tends to mime a held object with empty hands. hermoso_capabilities lists the exact credits for both"
},
"image": {
"type": "string",
"description": "who performs it: the actor/character image URL (@Image1). Required on kling. No one yet: generate_image a portrait of an AI person from words (no reference photo) and pass its url, or a saved / preset creator’s portrait (list_creators). Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
},
"video": {
"type": "string",
"description": "the reference video whose motion to re-perform"
},
"voice": {
"type": "string",
"description": "a NEW voice for the new performer: 'match' (a stock voice that fits them), 'female'/'male', or Aria, Sarah, Charlotte, George, Brian, Charlie. Omit = the clip's own sound. Never the original speaker's or a cloned voice"
},
"dryRun": {
"type": "boolean",
"description": "return the credits this exact job would hold and the engine auto picks, without rendering"
},
"engine": {
"enum": [
"auto",
"kling-edit",
"h3",
"seedance",
"wan",
"wan-prime",
"kling"
],
"type": "string",
"description": "default auto"
},
"images": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 8,
"description": "seedance/wan: more pictures, @Image2…: a character, product, outfit or place"
},
"prompt": {
"type": "string",
"description": "optional, e.g. 'same moves, new location: Tokyo at night'"
},
"language": {
"type": "string",
"description": "the new voice speaks this language (e.g. 'Spanish'), lip-synced to the new words; voice defaults to 'match'"
},
"faceRoute": {
"enum": [
"face_lane"
],
"type": "string",
"description": "'face_lane' = the user's \"I own the rights to this face\" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own."
},
"resolution": {
"enum": [
"480p",
"720p",
"1080p"
],
"type": "string",
"description": "a person swap defaults to 720p (seedance or kling-edit); otherwise seedance and wan default 1080p; h3 720p or 1080p, default the clip’s own"
},
"orientation": {
"enum": [
"video",
"image"
],
"type": "string",
"description": "kling only: which aspect to keep: the video's (default) or the image's"
}
}
}| dryRun | boolean | return the routing decision (single pass vs stitched acts, resolved model + act lengths) and the estimated credits the real render reserves, WITHOUT submitting a render — free, nothing charged |
| lockup | boolean | brand wordmark + tagline composited over the closing seconds. DEFAULT FALSE — set true ONLY when the user asks for branding on the close |
| creator | string | CAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”), or a PRESET AI creator from list_creators presets by exact name or id (free, no generation). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand (casting skips the portrait we would otherwise generate). Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default when the plan has a person on camera and the account is on a paid Hermoso plan (a free account gets a fresh AI person), and the read-back says `default:true`; pass "none" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, if an explicitly named creator is cast on a plan with nobody on camera, or if a REAL person is cast on a free plan. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded. |
| endCard | boolean | append the branded end card. DEFAULT FALSE on every recipe — set true ONLY when the user asks for an end card (a clone of a video that had none should not grow one) |
| inserts | array | the user's own screens laid over the finished video at these seconds (each item's look says how), sound running underneath (about 2 credits). The film always opens and ends on moving footage. Omit to use plan_ad's creative.inserts; [] renders without them. plan_ad with inserts writes the board around them. |
| screens | array | real screenshots of the app or site for the shots that film it on a screen (URLs, upload_file URLs or Library ids), laid over them as cards |
| captions | boolean | burn the plan's per-scene on-screen words as caption pills. DEFAULT FALSE on every recipe — set true ONLY when the user asks for on-screen text or captions; no recipe turns them on by itself |
| creative* | object | the FULL structured output of plan_ad (must contain video_storyboard) |
| ttsVoice | string | voiceover voice name (e.g. Rachel / George) when the plan voices over |
| faceRoute | string | 'face_lane' records the user's statement "I own the rights to this face" for the real person in their reference (paid plans; the likeness terms apply). Set it only when the user picked that option after a render said it could not use that face, and send the same request again; it then renders through Hermoso's face library at the normal price. Never set it on your own. The other options are a different `model` or one of the user's saved `creator`s. |
| textStyle | any | THE LOOK of captions and the end card, only with captions or endCard and only when the user described one: the look in WORDS ("chunky yellow comic letters, purple outline"), a preset (editorial: big serif title + small italic line; bold: condensed caps, outline; minimal; handwritten; boxed; pill, the default), or fields. "TITLE · small line" puts the part after the dot on a second line. |
| musicModel | string | compose the bed on minimax-music-3 or lyria-3.5, skipping the free library |
| resolution | string | Omit = the default tier, hermoso_capabilities defaultResolution (720p), the tier its `credits` prices are for. 1080p is ~2.25x, 4k more, 480p less: quote creditsByRes and pass the tier you quoted. A tier a model's `resolutions` lacks renders at its best one. |
| aspectRatio | string | output aspect ratio, e.g. 9:16 (default) / 1:1 / 16:9 |
| foundations | string | product commercial / cinematic: 'choose' paints ONLY the foundations (identity sheet + four-up moodboard, or hero + location) and returns them, no video — then call again with moodboardPick + foundationImages |
| screenChoice | string | the user's answer to needsAppScreens: brand = their saved app screens, site = found on their website (saved to the brand), without = none, device kept turned away |
| moodboardPick | integer | which moodboard panel (1-4, left to right, top to bottom) the storyboard follows; default 1 |
| durationSeconds | number | total ad length in seconds, 4–180 (clamped). Omit to keep the plan’s own length, almost always right: this only RE-TIMES the authored board; to change the ad’s length, re-run plan_ad with durationSeconds. A length that fits ONE clip renders in one pass; longer is stitched from acts of the model’s clip maximum (15s on most, 30s on the longest-clip one), remainder last. dryRun:true shows the split free. |
| foundationImages | object | images a foundations:'choose' call returned, reused as-is (not repainted) |
| allowGenericProduct | boolean | proceed even though this brand has NO product photo on file and the ad features a product — the packaging will be INVENTED. Only pass true after telling the user that and hearing they are fine with a generic stand-in |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"creative"
],
"properties": {
"board": {
"type": "boolean",
"description": "paint the storyboard board first (default: on for UGC and cinematic spots, off for a product-only commercial); false = render from the text shot list"
},
"model": {
"type": "string",
"description": "video model id from hermoso_capabilities (default: the engine the router picks for the plan: seedance-2.5 for every ad, a person talking to camera included). Naming one is a DELIBERATE pick — the server asks before ever swapping it (no silent fallback)"
},
"music": {
"type": [
"boolean",
"string"
],
"description": "music bed: OFF unless asked. true = the plan's own music line; or the bed in words ('lo-fi jazz, brushed drums'); false = none"
},
"dryRun": {
"type": "boolean",
"description": "return the routing decision (single pass vs stitched acts, resolved model + act lengths) and the estimated credits the real render reserves, WITHOUT submitting a render — free, nothing charged"
},
"lockup": {
"type": "boolean",
"description": "brand wordmark + tagline composited over the closing seconds. DEFAULT FALSE — set true ONLY when the user asks for branding on the close"
},
"creator": {
"type": "string",
"description": "CAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”), or a PRESET AI creator from list_creators presets by exact name or id (free, no generation). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand (casting skips the portrait we would otherwise generate). Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default when the plan has a person on camera and the account is on a paid Hermoso plan (a free account gets a fresh AI person), and the read-back says `default:true`; pass \"none\" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, if an explicitly named creator is cast on a plan with nobody on camera, or if a REAL person is cast on a free plan. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
},
"endCard": {
"type": "boolean",
"description": "append the branded end card. DEFAULT FALSE on every recipe — set true ONLY when the user asks for an end card (a clone of a video that had none should not grow one)"
},
"inserts": {
"type": "array",
"items": {
"type": "object",
"required": [
"image",
"start",
"end"
],
"properties": {
"end": {
"type": "number",
"description": "second it ends: at most 2 s after start (2.5 s full-screen), never in the last 1.5 s of the film"
},
"key": {
"type": "boolean",
"description": "true = the story needs this one (the payoff): never left out when there are more screens than the length takes"
},
"kind": {
"enum": [
"screen",
"photo"
],
"type": "string",
"description": "screen (default): an app, page, dashboard, document, review or chart. photo: a person, product or place, NOT laid over the video (a product photo goes in as the brand's product photo, a person as a saved creator)"
},
"look": {
"enum": [
"cutaway",
"card",
"full"
],
"type": "string",
"description": "cutaway (default): a hard cut to the screen filling the frame, cropped to focus, held still, never enlarged past 1.25x its pixels (any gap is the screenshot's own background colour; a screen too small to read is left out and said). card: picture-in-picture over the playing footage, clear of the speaker's face. full: the whole picture uncropped on a blurred backdrop. card and full ONLY when the user asked"
},
"focus": {
"type": "object",
"required": [
"x",
"y",
"w",
"h"
],
"properties": {
"h": {
"type": "number"
},
"w": {
"type": "number"
},
"x": {
"type": "number"
},
"y": {
"type": "number"
}
},
"description": "the readable part to fill the frame with, as fractions of the picture from its top-left (e.g. the flagged-items table); a crop as tall as wide, or taller, reads best on a phone. Omit for the whole picture, plain margins trimmed"
},
"image": {
"type": "string",
"description": "an https or upload_file URL, a Library id (L0, L1…) or a local path on the CLI"
},
"start": {
"type": "number",
"description": "second it appears, never before 1 s (the film opens on moving footage)"
}
}
},
"maxItems": 8,
"description": "the user's own screens laid over the finished video at these seconds (each item's look says how), sound running underneath (about 2 credits). The film always opens and ends on moving footage. Omit to use plan_ad's creative.inserts; [] renders without them. plan_ad with inserts writes the board around them."
},
"screens": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 6,
"description": "real screenshots of the app or site for the shots that film it on a screen (URLs, upload_file URLs or Library ids), laid over them as cards"
},
"captions": {
"type": "boolean",
"description": "burn the plan's per-scene on-screen words as caption pills. DEFAULT FALSE on every recipe — set true ONLY when the user asks for on-screen text or captions; no recipe turns them on by itself"
},
"creative": {
"type": "object",
"properties": {},
"description": "the FULL structured output of plan_ad (must contain video_storyboard)",
"additionalProperties": {}
},
"ttsVoice": {
"type": "string",
"description": "voiceover voice name (e.g. Rachel / George) when the plan voices over"
},
"faceRoute": {
"enum": [
"face_lane"
],
"type": "string",
"description": "'face_lane' records the user's statement \"I own the rights to this face\" for the real person in their reference (paid plans; the likeness terms apply). Set it only when the user picked that option after a render said it could not use that face, and send the same request again; it then renders through Hermoso's face library at the normal price. Never set it on your own. The other options are a different `model` or one of the user's saved `creator`s."
},
"textStyle": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"font": {
"type": "string",
"description": "sans|serif|elegant|condensed|hand or any Google Fonts family"
},
"size": {
"type": [
"string",
"number"
],
"description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction"
},
"tilt": {
"type": "number",
"description": "degrees, ±45"
},
"color": {
"type": "string",
"description": "any CSS colour"
},
"italic": {
"type": "boolean"
},
"preset": {
"type": "string"
},
"shadow": {
"type": "boolean"
},
"weight": {
"type": "number"
},
"outline": {
"type": "boolean"
},
"subFont": {
"type": "string"
},
"describe": {
"type": "string",
"description": "the look in words"
},
"position": {
"type": [
"string",
"number"
],
"description": "top|center|lower|bottom or 0.05-0.95 from the top"
},
"textCase": {
"enum": [
"as-is",
"upper",
"lower",
"title"
],
"type": "string"
},
"cardColor": {
"type": "string"
},
"subItalic": {
"type": "boolean"
},
"background": {
"type": "string",
"description": "none|pill|a colour"
},
"outlineColor": {
"type": "string"
},
"highlightColor": {
"type": "string",
"description": "word-timed subtitles only: the word being said is lit in this colour (preset highlight sets it)"
}
}
}
],
"description": "THE LOOK of captions and the end card, only with captions or endCard and only when the user described one: the look in WORDS (\"chunky yellow comic letters, purple outline\"), a preset (editorial: big serif title + small italic line; bold: condensed caps, outline; minimal; handwritten; boxed; pill, the default), or fields. \"TITLE · small line\" puts the part after the dot on a second line."
},
"musicModel": {
"type": "string",
"description": "compose the bed on minimax-music-3 or lyria-3.5, skipping the free library"
},
"resolution": {
"enum": [
"480p",
"720p",
"1080p",
"4k"
],
"type": "string",
"description": "Omit = the default tier, hermoso_capabilities defaultResolution (720p), the tier its `credits` prices are for. 1080p is ~2.25x, 4k more, 480p less: quote creditsByRes and pass the tier you quoted. A tier a model's `resolutions` lacks renders at its best one."
},
"aspectRatio": {
"type": "string",
"description": "output aspect ratio, e.g. 9:16 (default) / 1:1 / 16:9"
},
"foundations": {
"enum": [
"choose"
],
"type": "string",
"description": "product commercial / cinematic: 'choose' paints ONLY the foundations (identity sheet + four-up moodboard, or hero + location) and returns them, no video — then call again with moodboardPick + foundationImages"
},
"screenChoice": {
"enum": [
"brand",
"site",
"without"
],
"type": "string",
"description": "the user's answer to needsAppScreens: brand = their saved app screens, site = found on their website (saved to the brand), without = none, device kept turned away"
},
"moodboardPick": {
"type": "integer",
"maximum": 4,
"minimum": 1,
"description": "which moodboard panel (1-4, left to right, top to bottom) the storyboard follows; default 1"
},
"durationSeconds": {
"type": "number",
"description": "total ad length in seconds, 4–180 (clamped). Omit to keep the plan’s own length, almost always right: this only RE-TIMES the authored board; to change the ad’s length, re-run plan_ad with durationSeconds. A length that fits ONE clip renders in one pass; longer is stitched from acts of the model’s clip maximum (15s on most, 30s on the longest-clip one), remainder last. dryRun:true shows the split free."
},
"foundationImages": {
"type": "object",
"properties": {
"hero": {
"type": "string",
"description": "hero image URL from a foundations:'choose' call; reused as-is"
},
"location": {
"type": "string",
"description": "location image URL from a foundations:'choose' call; reused as-is"
},
"moodboard": {
"type": "string",
"description": "moodboard image URL from a foundations:'choose' call; reused as-is"
},
"identitySheet": {
"type": "string",
"description": "identity sheet image URL from a foundations:'choose' call; reused as-is"
}
},
"description": "images a foundations:'choose' call returned, reused as-is (not repainted)"
},
"allowGenericProduct": {
"type": "boolean",
"description": "proceed even though this brand has NO product photo on file and the ad features a product — the packaging will be INVENTED. Only pass true after telling the user that and hearing they are fine with a generic stand-in"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"image"
],
"properties": {
"name": {
"type": "string",
"description": "the asset name the files are named after"
},
"image": {
"type": "string",
"description": "the ad: an https / upload_file URL, a Library id (L0…), or a local path on the CLI"
},
"dryRun": {
"type": "boolean",
"description": "price only: nothing made, nothing charged"
},
"formats": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 8,
"description": "default ['1:1','4:5','9:16','16:9']"
},
"fixLabel": {
"type": "boolean",
"description": "false = no product-label check (default on when the ad shows the brand's saved product)"
},
"variants": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"edit": {
"type": "string"
},
"name": {
"type": "string"
},
"swap": {
"type": "array",
"items": {
"type": "object",
"required": [
"from",
"to"
],
"properties": {
"to": {
"type": "string"
},
"from": {
"type": "string"
}
}
}
},
"headline": {
"type": "string"
},
"language": {
"type": "string"
}
}
}
]
},
"maxItems": 6,
"description": "the versions besides the original; a bare string is an edit in words"
}
}
}| the video length in seconds, when you know it: it prices exactly that, and anything past it is not read |
| language | string | the language to write in, when not English |
| transcript | string | the transcript text, when you already have it; then nothing is read |
| instructions | string | your direction for every piece: audience, tone, length, a call to action |
| includeTranscript | boolean | also return the transcript that was read |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"read": {
"enum": [
"auto",
"watch"
],
"type": "string",
"description": "'watch' reads a YouTube video by watching it rather than from its captions (one with no captions)"
},
"model": {
"type": "string",
"description": "a Claude writing-model id from hermoso_capabilities; omit for the default"
},
"video": {
"type": "string",
"description": "a YouTube link, a video file URL, a Library id (L0…), an upload_file URL or a social post link"
},
"confirm": {
"type": "boolean",
"description": "true only after the user agreed to the quoted price"
},
"formats": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 6,
"description": "summary, chapters, chapter_summaries, thread, blog, quotes, or any piece in your own words; default summary"
},
"seconds": {
"type": "number",
"description": "the video length in seconds, when you know it: it prices exactly that, and anything past it is not read"
},
"language": {
"type": "string",
"description": "the language to write in, when not English"
},
"transcript": {
"type": "string",
"description": "the transcript text, when you already have it; then nothing is read"
},
"instructions": {
"type": "string",
"description": "your direction for every piece: audience, tone, length, a call to action"
},
"includeTranscript": {
"type": "boolean",
"description": "also return the transcript that was read"
}
}
}| languages | array | dub into these languages (paid), e.g. ['Spanish','de']; the original is kept too |
| placements | array | default ['everywhere']; a canvas, a WxH size, a platform word, or {placement, reframe} |
| wordsPerCue | integer | captions: 1-8 words a cue (default 4) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video"
],
"properties": {
"pad": {
"type": "object",
"properties": {
"dim": {
"type": "number"
},
"blur": {
"type": "number"
},
"fill": {
"type": "string"
},
"position": {
"type": [
"string",
"number"
]
}
},
"description": "the pad's look: fill 'blur' (default) or a #rrggbb colour, blur 0-1, dim 0-0.6, position top/center/bottom or 0-1"
},
"name": {
"type": "string",
"description": "the asset name the files are named after"
},
"video": {
"type": "string",
"description": "the video: an https / upload_file URL, a Library id (L0…), or a local path on the CLI"
},
"dryRun": {
"type": "boolean",
"description": "price only: nothing made, nothing charged"
},
"reframe": {
"enum": [
"pad",
"crop",
"ai"
],
"type": "string",
"description": "default 'pad' (free); 'ai' is paid and only when the user asks"
},
"captions": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string"
},
{
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
],
"description": "true = word-timed captions on every file (only when the user asks); a textStyle preset name or object sets the look"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 4,
"description": "dub into these languages (paid), e.g. ['Spanish','de']; the original is kept too"
},
"placements": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"label": {
"type": "string"
},
"width": {
"type": "number"
},
"height": {
"type": "number"
},
"reframe": {
"enum": [
"pad",
"crop",
"ai"
],
"type": "string"
},
"placement": {
"type": "string"
}
}
}
]
},
"maxItems": 8,
"description": "default ['everywhere']; a canvas, a WxH size, a platform word, or {placement, reframe}"
},
"wordsPerCue": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "captions: 1-8 words a cue (default 4)"
}
}
}| resolution | string | Seedance and Wan; default 1080p |
| styleImages | array | pictures that ARE the look (their style, never their subject) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"video"
],
"properties": {
"style": {
"type": "string",
"description": "a look id from hermoso_capabilities (e.g. claymation, knitted-yarn, cel-shaded-anime-cg)"
},
"video": {
"type": "string",
"description": "the source video URL (a render, job result or list_library)"
},
"engine": {
"enum": [
"auto",
"seedance",
"wan",
"wan-prime",
"kling"
],
"type": "string",
"description": "default auto"
},
"describe": {
"type": "string",
"description": "a look in the user’s own words, when no preset fits, or extra detail on top of a preset"
},
"faceRoute": {
"enum": [
"face_lane"
],
"type": "string",
"description": "'face_lane' = the user's \"I own the rights to this face\" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own."
},
"characters": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 4,
"description": "characters to draw the people as. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
},
"resolution": {
"enum": [
"480p",
"720p",
"1080p"
],
"type": "string",
"description": "Seedance and Wan; default 1080p"
},
"styleImages": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 9,
"description": "pictures that ARE the look (their style, never their subject)"
}
}
}| voice |
| string |
| a default voice name for this persona (engines + voices are in hermoso_capabilities) |
| source | string | "generated" (default) = an AI-made person; "upload" / "social" = a REAL person |
| useAnyway | boolean | only for a creator whose saved photo was flagged too unclear to cast (render_ad says so): true casts the current photo as it is, no new image needed |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"look": {
"type": "string",
"description": "their canonical wardrobe/appearance in words — reused to hold the look steady across ads"
},
"name": {
"type": "string",
"description": "what to call this creator (e.g. “Sarah”) — list_creators and the app’s picker match on it"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"image": {
"type": "string",
"description": "REQUIRED except with useAnyway. public https url of the portrait (an existing render’s url, or any public photo). Not a local file path — upload it with upload_file first and save the url that returns"
},
"poses": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 4 extra full-body / angle plates of the SAME person (public urls) — they make a wider shot hold the identity"
},
"voice": {
"type": "string",
"description": "a default voice name for this persona (engines + voices are in hermoso_capabilities)"
},
"source": {
"enum": [
"generated",
"upload",
"social"
],
"type": "string",
"description": "\"generated\" (default) = an AI-made person; \"upload\" / \"social\" = a REAL person"
},
"useAnyway": {
"type": "boolean",
"description": "only for a creator whose saved photo was flagged too unclear to cast (render_ad says so): true casts the current photo as it is, no new image needed"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"icon": {
"type": "string",
"description": "one emoji"
},
"title": {
"type": "string",
"description": "title of the new Notion page"
},
"content": {
"type": "string",
"description": "markdown body"
},
"databaseId": {
"type": "string",
"description": "database id or link: saves as a new row"
},
"dataSourceId": {
"type": "string",
"description": "data source id to save into as a new row; pass only one of parentPageId, databaseId, dataSourceId"
},
"parentPageId": {
"type": "string",
"description": "page id or link to save under"
},
"includeSchedule": {
"type": "boolean",
"description": "add the scheduled posts"
},
"swipefileCollection": {
"type": "string",
"description": "swipefile collection name or id"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "max ads returned (1–25, default 8)"
},
"query": {
"type": "string",
"description": "keyword search across ALL advertisers (use INSTEAD of companyName/pageId)"
},
"domain": {
"type": "string",
"description": "the advertiser’s website, e.g. buffer.com — with companyName, used to confirm which Meta page is really theirs"
},
"pageId": {
"type": "string",
"description": "one advertiser’s ads by Facebook page id (most precise)"
},
"status": {
"enum": [
"ACTIVE",
"INACTIVE",
"ALL"
],
"type": "string",
"description": "ACTIVE = currently running; default ALL (includes proven past winners)"
},
"country": {
"type": "string",
"description": "2-letter code or 'ALL' (default ALL)"
},
"mediaType": {
"enum": [
"ALL",
"IMAGE",
"VIDEO",
"MEME",
"IMAGE_AND_MEME",
"NONE"
],
"type": "string",
"description": "filter by creative type (default ALL)"
},
"companyName": {
"type": "string",
"description": "one advertiser’s ads by brand name — resolved to the brand’s own page only on evidence (its Instagram, its domain, or ads that land on its domain); pass domain too for the best match"
}
}
}| runNow | boolean | check now (spends now) |
| topics | array | niche search phrases, e.g. "protein breakfast" |
| cadence | string | weekly (default) or daily check |
| modules | object | on/off; omitted keep theirs |
| competitors | array | the COMPLETE brand list |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"auto": {
"type": "boolean",
"description": "true picks competitors, similar brands and topics from the saved brand or seed; bills one model call at its token cost"
},
"seed": {
"type": "string",
"description": "for auto: a website, brand or niche"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"email": {
"type": "boolean",
"description": "the weekly email"
},
"force": {
"type": "boolean",
"description": "with runNow: check again right after a check, allowed once what is watched changed"
},
"dryRun": {
"type": "boolean",
"description": "true quotes one check and saves nothing"
},
"runNow": {
"type": "boolean",
"description": "check now (spends now)"
},
"topics": {
"type": "array",
"items": {
"type": "string"
},
"description": "niche search phrases, e.g. \"protein breakfast\""
},
"cadence": {
"enum": [
"weekly",
"daily"
],
"type": "string",
"description": "weekly (default) or daily check"
},
"modules": {
"type": "object",
"properties": {
"ideas": {
"type": "boolean",
"description": "true includes three ideas to make from the findings"
},
"newAds": {
"type": "boolean",
"description": "true includes new ads the watched brands launched"
},
"trends": {
"type": "boolean",
"description": "true includes formats trending in the topics"
},
"winners": {
"type": "boolean",
"description": "true includes watched ads that have run 30 days"
},
"topPosts": {
"type": "boolean",
"description": "true includes the most-viewed posts for the topics"
}
},
"description": "on/off; omitted keep theirs"
},
"competitors": {
"type": "array",
"items": {
"type": "object",
"required": [
"name"
],
"properties": {
"kind": {
"enum": [
"competitor",
"similar"
],
"type": "string",
"description": "competitor (head-to-head rival) or similar (adjacent brand)"
},
"name": {
"type": "string",
"description": "brand name to watch"
},
"domain": {
"type": "string",
"description": "e.g. ridge.com: Google is read by domain, and it finds the right Meta page"
}
},
"additionalProperties": {}
},
"description": "the COMPLETE brand list"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"type": {
"enum": [
"view",
"edit",
"embed"
],
"type": "string",
"description": "view (default), edit, or embed (personal OneDrive only)"
},
"scope": {
"enum": [
"anonymous",
"organization",
"users"
],
"type": "string",
"description": "anonymous (default, anyone with the link), organization (work or school), or users (invited people only)"
},
"fileId": {
"type": "string",
"description": "OneDrive file or folder id from list_onedrive_files"
},
"confirm": {
"type": "boolean",
"description": "required for an anonymous edit link"
},
"password": {
"type": "string",
"description": "personal OneDrive with Microsoft 365"
},
"expiresAt": {
"type": "string",
"description": "e.g. 2026-11-30 or 2026-11-30T17:00:00Z"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"scenes"
],
"properties": {
"model": {
"type": "string",
"description": "video model id from hermoso_capabilities — omit to let the router pick"
},
"voice": {
"type": "string",
"description": "voiceover voice name, e.g. Rachel / George"
},
"scenes": {
"type": "array",
"items": {
"type": "object",
"properties": {},
"additionalProperties": {}
},
"minItems": 2,
"description": "array of scene objects (visual + optional voiceover/seconds)"
},
"voiceover": {
"type": "string",
"description": "full voiceover script spoken across the scenes"
},
"resolution": {
"type": "string",
"description": "720p (default), 1080p for full detail, or 480p for a cheaper draft"
},
"aspectRatio": {
"type": "string",
"description": "output aspect ratio, e.g. 9:16 (default) / 1:1 / 16:9"
},
"durationSeconds": {
"type": "number",
"description": "total spot length in seconds (defaults to the sum of the scenes’ seconds)"
}
}
}| dryRun | boolean | true = price only, nothing looked up or charged |
| market | string | 2-letter country the posts are read from (default US) |
| brandId | string | a profile id/name from list_brands; omit for the active profile |
| product | string | the product (a name from list_products, or words); omit to use the saved brand |
| category | string | the niche in words, e.g. "matcha", "running shoes" |
| hashtags | array | your own hashtags (up to 5, # optional) |
| keywords | array | your own search words (up to 4), instead of the derived ones |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"to": {
"enum": [
"render_ad",
"make_series"
],
"type": "string",
"description": "where the pick goes: render_ad (plan_ad then render_ad, the default) or make_series"
},
"goal": {
"type": "string",
"description": "what the posts should do: sales, followers, awareness…"
},
"pick": {
"type": "object",
"properties": {},
"description": "THE PICK: one idea object exactly as this tool returned it in `ideas`",
"additionalProperties": {}
},
"reads": {
"type": "integer",
"maximum": 8,
"minimum": 0,
"description": "how many of the strongest posts to read for their spoken hook, 0-8 (default 4)"
},
"claims": {
"type": "array",
"items": {
"type": "string"
},
"description": "with pick: extra claims the USER stated about their product (added to the brand facts)"
},
"dryRun": {
"type": "boolean",
"description": "true = price only, nothing looked up or charged"
},
"market": {
"type": "string",
"description": "2-letter country the posts are read from (default US)"
},
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands; omit for the active profile"
},
"product": {
"type": "string",
"description": "the product (a name from list_products, or words); omit to use the saved brand"
},
"category": {
"type": "string",
"description": "the niche in words, e.g. \"matcha\", \"running shoes\""
},
"hashtags": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 5,
"description": "your own hashtags (up to 5, # optional)"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 4,
"description": "your own search words (up to 4), instead of the derived ones"
}
}
}| website domain |
| summary | string | one-line description |
| audience | string | who the brand sells to, one line |
| category | string | product or business category, one line |
| logoDark | string | for light pictures |
| logoLight | string | for dark pictures; "none" removes |
| pronounce | string | how the brand NAME is said aloud, as a simple respelling with the stressed syllable in capitals (e.g. "KOH-dee-ak"). Videos use it as a delivery note beside the spoken line; set it when a render mispronounced the name. |
| positioning | string | how the brand is positioned against alternatives, one line |
| profileType | string | website, social, describe or explore; explore keeps the profile but stops using it |
| pronunciations | object | how PRODUCT names are said aloud, e.g. {"Power Cakes": "POW-er cakes"}. Merged into the saved ones; an empty string removes one. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"goal": {
"type": "string",
"description": "current marketing goal"
},
"logo": {
"type": "string",
"description": "logo image: URL or local file path to a PNG, JPEG or WebP (no SVG)"
},
"name": {
"type": "string",
"description": "brand name"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"sells": {
"type": "string",
"description": "what the brand sells"
},
"style": {
"type": "string",
"description": "visual style — palette, typography, aesthetic"
},
"voice": {
"type": "string",
"description": "brand voice/tone"
},
"domain": {
"type": "string",
"description": "website domain"
},
"summary": {
"type": "string",
"description": "one-line description"
},
"audience": {
"type": "string",
"description": "who the brand sells to, one line"
},
"category": {
"type": "string",
"description": "product or business category, one line"
},
"logoDark": {
"type": "string",
"description": "for light pictures"
},
"logoLight": {
"type": "string",
"description": "for dark pictures; \"none\" removes"
},
"pronounce": {
"type": "string",
"description": "how the brand NAME is said aloud, as a simple respelling with the stressed syllable in capitals (e.g. \"KOH-dee-ak\"). Videos use it as a delivery note beside the spoken line; set it when a render mispronounced the name."
},
"positioning": {
"type": "string",
"description": "how the brand is positioned against alternatives, one line"
},
"profileType": {
"enum": [
"website",
"social",
"describe",
"explore"
],
"type": "string",
"description": "website, social, describe or explore; explore keeps the profile but stops using it"
},
"pronunciations": {
"type": "object",
"description": "how PRODUCT names are said aloud, e.g. {\"Power Cakes\": \"POW-er cakes\"}. Merged into the saved ones; an empty string removes one.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
}
}
}| documentId |
| string |
| the document id (from create_doc, or list_drive_files for one the user picked) |
| confirmCells | number | echo back the character count the unconfirmed call reported (rewrite only) |
| replacements | array | find/replace pairs, applied in order |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"docUrl": {
"type": "string",
"description": "a Google Docs URL — the id is extracted from it"
},
"confirm": {
"type": "boolean",
"description": "true runs a whole-body rewrite after the character count is shown"
},
"rewrite": {
"type": "string",
"description": "replace the WHOLE body with this text (\"\" empties the doc)"
},
"dropdowns": {
"type": "array",
"items": {
"type": "object",
"properties": {
"tabId": {
"type": "string",
"description": "tab id holding the dropdown, when the document has several tabs"
},
"title": {
"type": "string",
"description": "the dropdown title (from read_doc)"
},
"value": {
"type": "string",
"description": "the option to select, by its display text"
},
"options": {
"type": "array",
"items": {
"type": "string"
},
"description": "change the dropdown itself: the COMPLETE new option list (2–50) by display text; existing options keep their colour; shared by every chip built from it"
},
"replace": {
"type": "object",
"description": "for an option you remove that a chip shows: {\"Old\": \"New\"}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"newTitle": {
"type": "string",
"description": "rename the dropdown"
},
"dropdownId": {
"type": "string",
"description": "when two dropdowns share a title"
}
}
},
"description": "dropdown chips to set, or dropdowns to change"
},
"documentId": {
"type": "string",
"description": "the document id (from create_doc, or list_drive_files for one the user picked)"
},
"confirmCells": {
"type": "number",
"description": "echo back the character count the unconfirmed call reported (rewrite only)"
},
"replacements": {
"type": "array",
"items": {
"type": "object",
"required": [
"find"
],
"properties": {
"find": {
"type": "string",
"description": "exact text to find, as read_doc shows it; zero matches is reported as a failure"
},
"replace": {
"type": "string",
"description": "text to put in place of each match"
},
"matchCase": {
"type": "boolean",
"description": "false ignores letter case when matching; default matches case"
}
}
},
"description": "find/replace pairs, applied in order"
}
}
}| Google spreadsheet id; use this or sheetUrl |
| valueInputOption | string | USER_ENTERED (default) parses formulas, dates and numbers the way typing them would; RAW stores every value as literal text |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"range": {
"type": "string",
"description": "A1 range or anchor cell, e.g. \"B2:C5\", \"B2\", or \"Q3 Report!B2\" (default A1)"
},
"values": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": [
"string",
"number",
"boolean"
]
}
},
"description": "array of row arrays to write"
},
"confirm": {
"type": "boolean",
"description": "required only when the target range already holds values"
},
"updates": {
"type": "array",
"items": {
"type": "object",
"properties": {
"range": {
"type": "string",
"description": "A1 range or anchor cell for this block, e.g. B2:C5 or Q3!B2"
},
"values": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": [
"string",
"number",
"boolean"
]
}
},
"description": "rows for this block, as an array of row arrays"
}
}
},
"description": "write SEVERAL disjoint ranges in one call, instead of range+values"
},
"sheetUrl": {
"type": "string",
"description": "Google Sheets link, instead of spreadsheetId"
},
"spreadsheetId": {
"type": "string",
"description": "Google spreadsheet id; use this or sheetUrl"
},
"valueInputOption": {
"enum": [
"USER_ENTERED",
"RAW"
],
"type": "string",
"description": "USER_ENTERED (default) parses formulas, dates and numbers the way typing them would; RAW stores every value as literal text"
}
}
}| boolean |
| ASK FOR A ONE-TIME UPLOAD URL instead of uploading now — use this whenever the file is on the user’s machine and you can run a shell or an HTTP request. Returns a uploadUrl you PUT the raw bytes to (any HTTP client), which answers with the durable Hermoso url. It beats `dataUri` for anything but a small image: a data: URI spends the whole file as tokens in this conversation. One file per url, and it expires. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "a PUBLIC http(s) URL Hermoso fetches server-side (private/internal addresses are refused, and every redirect hop is re-checked). Works on every surface including the hosted connector, and the bytes never cross this connection — prefer this whenever the file is reachable on the web."
},
"name": {
"type": "string",
"description": "original file name — helps pick the right extension"
},
"dataUri": {
"type": "string",
"description": "base64 data: URI of the file bytes (data:<mime>;base64,<…>) — bytes travel over this connection, so keep it small"
},
"getUploadUrl": {
"type": "boolean",
"description": "ASK FOR A ONE-TIME UPLOAD URL instead of uploading now — use this whenever the file is on the user’s machine and you can run a shell or an HTTP request. Returns a uploadUrl you PUT the raw bytes to (any HTTP client), which answers with the durable Hermoso url. It beats `dataUri` for anything but a small image: a data: URI spends the whole file as tokens in this conversation. One file per url, and it expires."
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"end": {
"type": "number",
"description": "window end in seconds"
},
"fps": {
"type": "number",
"description": "frames per second in the window (default ~16 frames across it)"
},
"url": {
"type": "string",
"description": "the video"
},
"save": {
"type": "boolean",
"description": "true = save the frames at times / clips as full-size stills and return their URLs"
},
"clips": {
"type": "array",
"items": {
"type": "number"
},
"description": "save: one still from the middle of each numbered clip (split at hard cuts; negative counts from the end)"
},
"start": {
"type": "number",
"description": "window start in seconds"
},
"times": {
"type": "array",
"items": {
"type": "number"
},
"description": "exact seconds instead of a window (max 48; max 8 with save)"
}
}
}