← The evidence ledger
Evidence row 4360747
Observed , by probe.mcp/1.
- Kind
- mcp.tools
- About
- endpoint b7b10d45-f6fb-49e6-abb9-091c4a879468
- Recorded
- Previous row
- #4139974
- Body SHA-256
- 1bc4966bb3c588a0ccd6d0281628220d52f542ec60ce1a49b1e181f53523356a
- Leaf
- 0xff2ca36de2586600bb3cec571fdfc63ce102018fdf975d719777fbbd3bd9cce6
What was observed
{
"count": 129,
"tools": [
{
"description": "[writes flayr data · `write` scope] Re-reads a connected account's handle, display name and avatar from the network (use after the user renames the account) and returns the stored values. Changes nothing else: no connect, disconnect or post. If the status is not `updated`, tell the user to reconnect the account at the returned `reconnectUrl`.\n(Wraps POST /api/v2/accounts/{id}/refresh.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "The brand the account is connected to, as for GET /api/v2/accounts.",
"type": "string"
},
"id": {
"description": "An account id from GET /api/v2/accounts.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "account_refresh"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] approve lets a rule's reshare / comment go out at its time; retry re-sends a failed one (same idempotency key); reject / cancel stop it. Confirm with the user before approving. Needs the `publish` scope (or admin) on the Agency plan or higher.\n(Wraps POST /api/v2/automations/actions/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"op": {
"enum": [
"approve",
"reject",
"retry",
"cancel"
],
"type": "string"
}
},
"required": [
"id",
"op"
],
"type": "object"
},
"name": "act_on_publish_action"
},
{
"description": "[writes flayr data · `write` scope] Checks an ad's cut against the ad-cut rules: too many joins (warning), joins only at a rest point, no cut inside a caption or its fade, one caption layer, no audio dip at a join, exactly one ending, and the network's length / aspect / ad-text window (TikTok 9:16, 12-25 s, ad text up to 100 by default). Each finding names its fix. It reads the cut you describe (segments, captions, signals), not a rendered file; a rule with nothing to check is reported unchecked. With contentId the result is kept for that draft: a format:ad video draft is booked or filed as an ad only once its latest lint for the network passes.\n(Wraps POST /api/v2/video-studio/ad-cut/lint.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"adText": {
"description": "Ignored with contentId (the draft text is used).",
"maxLength": 5000,
"type": "string"
},
"aspect": {
"description": "For example \"9:16\", or `{ width, height }`."
},
"audioEnvelope": {
"description": "Output-time `[{ atMs, db }]`, per-source `{ sourceId: [{ atMs, db }] }`, or null for no audio."
},
"captions": {
"description": "`{ startMs, endMs, text?, layer?, fadeInMs?, fadeOutMs? }` in output ms. Omit when unknown; [] for none.",
"items": {
"type": "object"
},
"maxItems": 200,
"type": "array"
},
"contentId": {
"description": "Keep the result for this draft (write scope).",
"type": "string"
},
"joins": {
"description": "Extra cut points inside segments, output ms.",
"items": {
"type": "number"
},
"maxItems": 200,
"type": "array"
},
"network": {
"description": "tiktok, x, pinterest, reddit, or a configured network.",
"type": "string"
},
"segments": {
"description": "In order: takes `{ sourceId, inMs, outMs, includesEnding? }` or `{ kind: \"end_card\", durationMs }`.",
"items": {
"type": "object"
},
"maxItems": 100,
"type": "array"
},
"signals": {
"description": "Per source: `{ motion?: [{ atMs, energy }], envelope?: [{ atMs, db }] }`, in source ms.",
"type": "object"
}
},
"required": [
"network",
"segments"
],
"type": "object"
},
"name": "ad_cut_lint"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] Schedules comments on a post, sent once after it is live on that platform and its tokens resolve ({{postUrl}}, {{parentPostUrl:<post id>}}, {{youtubeUrl:<post id>}}), optionally at sendAt. Confirm with the user first. Needs the `publish` scope (or admin) on the Agency plan or higher.\n(Wraps POST /api/v2/posts/{id}/comments.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"comments": {
"items": {
"properties": {
"accountId": {
"description": "Default: the account the post went out through on that platform.",
"type": "string"
},
"delayMinutes": {
"minimum": 0,
"type": "integer"
},
"platform": {
"type": "string"
},
"sendAt": {
"description": "Not before this time.",
"format": "date-time",
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"platform",
"text"
],
"type": "object"
},
"maxItems": 20,
"type": "array"
},
"id": {
"type": "string"
}
},
"required": [
"id",
"comments"
],
"type": "object"
},
"name": "add_post_comments"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] Appends drafts (content ids) to the end of a queue — they will be BOOKED AND POSTED on the queue's schedule. Use `contentIds`, or `items` for a per-item YouTube title or platforms. Drafts of another brand, duplicates, archived drafts and drafts without a video for a video platform come back in `rejected`. Needs the `publish` scope (or admin) on the Agency plan or higher.\n(Wraps POST /api/v2/queues/{id}/items.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"type": "string"
},
"contentIds": {
"items": {
"type": "string"
},
"maxItems": 200,
"type": "array"
},
"id": {
"type": "string"
},
"items": {
"items": {
"properties": {
"contentId": {
"type": "string"
},
"instanceLabels": {
"description": "Internal labels per platform/account instance. An array of {platform, accountId?, labels}; accountId is the Zernio connected account id. Omit accountId when unknown. instanceKey is derived as canonical platform + \":account:\" + URL-encoded account id, or \":pending\" when unknown (twitter canonicalizes to x). Replaces the instance set when editing; [] clears it. Never published.",
"items": {
"additionalProperties": false,
"properties": {
"accountId": {
"maxLength": 200,
"type": "string"
},
"labels": {
"items": {
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"platform": {
"type": "string"
}
},
"required": [
"platform",
"labels"
],
"type": "object"
},
"maxItems": 20,
"type": "array"
},
"labels": {
"items": {
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"platforms": {
"description": "Post this item to these platforms instead of the queue's.",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"youtubeTitle": {
"description": "The video's YouTube title. Omitted → the draft's stored title, else derived from its caption.",
"maxLength": 100,
"type": "string"
}
},
"required": [
"contentId"
],
"type": "object"
},
"maxItems": 200,
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "add_queue_items"
},
{
"description": "[writes · `ads:spend` scope] Sets the Meta ad account spending limit, in the account currency (e.g. 800 = $800). Removing the limit (null) always needs the owner. Inside the owner's agent ceiling it is applied at once; above it the result is `approval_requested`: tell the user the owner has an approval request in Ads > Agent activity, and repeat the same call once they approve. Pass agentName and reason; both are shown to the owner.\n(Wraps POST /api/v2/ads/accounts/spend-limit.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"agentName": {
"description": "Which agent is acting. Shown to the owner in Ads > Agent activity.",
"type": "string"
},
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand.",
"type": "string"
},
"reason": {
"description": "Why the agent is doing this. Shown to the owner and kept in the audit trail.",
"type": "string"
},
"spendCap": {
"description": "The new limit in the ad account currency, or null to remove it.",
"type": [
"number",
"null"
]
}
},
"required": [
"agentName",
"reason",
"spendCap"
],
"type": "object"
},
"name": "ads_account_spend_limit_set"
},
{
"description": "[read-only] The brand's ad account with its currency, spend limit, balance (minor units) and readiness.\n(Wraps GET /api/v2/ads/accounts.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
}
},
"type": "object"
},
"name": "ads_accounts_list"
},
{
"description": "[writes · `ads:manage` scope] Attaches a Meta dataset (pixel) to an existing ad set, optionally with the conversion event (e.g. PURCHASE). Pass agentName and reason. Audited in Ads > Agent activity. It cannot be undone from there: to change it back, call this tool again with the earlier settings.\n(Wraps POST /api/v2/ads/ad-sets/{id}/dataset.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"agentName": {
"description": "Which agent is acting. Shown to the owner in Ads > Agent activity.",
"type": "string"
},
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand.",
"type": "string"
},
"customEventType": {
"description": "Meta standard event, e.g. PURCHASE, LEAD.",
"type": "string"
},
"id": {
"type": "string"
},
"pixelId": {
"description": "The Meta dataset (pixel) id, digits.",
"type": "string"
},
"reason": {
"description": "Why the agent is doing this. Shown to the owner and kept in the audit trail.",
"type": "string"
}
},
"required": [
"id",
"agentName",
"reason",
"pixelId"
],
"type": "object"
},
"name": "ads_ad_set_dataset"
},
{
"description": "[writes · `ads:manage` scope] Changes where an existing ad set runs. `excludeAudienceNetwork: true` keeps Facebook, Instagram and Messenger and drops Audience Network; or pass an explicit publisherPlatforms list. `id` is the ad set id from ads_campaign_get. Pass agentName and reason. Audited in Ads > Agent activity. It cannot be undone from there: to change it back, call this tool again with the earlier settings.\n(Wraps POST /api/v2/ads/ad-sets/{id}/placements.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"agentName": {
"description": "Which agent is acting. Shown to the owner in Ads > Agent activity.",
"type": "string"
},
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand.",
"type": "string"
},
"excludeAudienceNetwork": {
"type": "boolean"
},
"id": {
"type": "string"
},
"publisherPlatforms": {
"items": {
"enum": [
"facebook",
"instagram",
"messenger",
"threads",
"audience_network",
"whatsapp"
],
"type": "string"
},
"type": "array"
},
"reason": {
"description": "Why the agent is doing this. Shown to the owner and kept in the audit trail.",
"type": "string"
}
},
"required": [
"id",
"agentName",
"reason"
],
"type": "object"
},
"name": "ads_ad_set_placements"
},
{
"description": "[read-only] Recent agent actions on ads (agent name, reason, outcome), owner approval requests still pending, and the owner's agent ceilings. Use it to see whether an approval request was decided.\n(Wraps GET /api/v2/ads/agent/activity.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand.",
"type": "string"
}
},
"type": "object"
},
"name": "ads_agent_activity"
},
{
"description": "[read-only] The brand's published posts ranked for a boost (reach, engagement rate, link clicks, recency), with score parts, the call-to-action link found, and whether an ad account is connected for the network. Removed posts, posts with no call to action and networks a boost can't run on are left out. Read-only: nothing is boosted or charged.\n(Wraps GET /api/v2/ads/boost-candidates.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. Omitted: the key's brand (a brand-bound key) or the owner's active brand.",
"type": "string"
},
"days": {
"default": 30,
"description": "How many days back to read (clamped).",
"maximum": 90,
"minimum": 1,
"type": "integer"
},
"include": {
"description": "Also list posts on networks a boost can't run on.",
"enum": [
"unboostable"
],
"type": "string"
},
"limit": {
"default": 10,
"description": "How many candidates to return (clamped).",
"maximum": 50,
"minimum": 1,
"type": "integer"
}
},
"type": "object"
},
"name": "ads_boost_candidates"
},
{
"description": "[writes · `ads:manage` scope] Approves a proposed ad campaign. With activate: true it also switches it on (needs ads:spend too), through the normal activation checks, when its daily budget is within the owner's agent activation ceiling. Inside the owner's agent ceiling it is applied at once; above it the result is `approval_requested`: tell the user the owner has an approval request in Ads > Agent activity, and repeat the same call once they approve. Pass agentName and reason; both are shown to the owner. Confirm with the user before activating.\n(Wraps POST /api/v2/ads/campaigns/{id}/approve.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"activate": {
"description": "Also switch it on. Needs ads:spend as well.",
"type": "boolean"
},
"agentName": {
"description": "Which agent is acting. Shown to the owner in Ads > Agent activity.",
"type": "string"
},
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand.",
"type": "string"
},
"id": {
"type": "string"
},
"idempotencyKey": {
"description": "Optional; repeats with the same key do not re-run the activation.",
"type": "string"
},
"reason": {
"description": "Why the agent is doing this. Shown to the owner and kept in the audit trail.",
"type": "string"
}
},
"required": [
"id",
"agentName",
"reason"
],
"type": "object"
},
"name": "ads_campaign_approve"
},
{
"description": "[read-only] One ad campaign by its Flayr id, with its ad sets and ads, including each ad's network review status, reason when available, and last sync time. Do not invent a rejection reason when the network supplies none.\n(Wraps GET /api/v2/ads/campaigns/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "ads_campaign_get"
},
{
"description": "[writes flayr data · `write` scope] Pauses a live ad campaign on its ad network so it stops spending (`action: \"pause\"` or `\"stop\"`; both leave it paused). To approve and switch on a proposed campaign use ads_campaign_approve. Confirm with the user first.\n(Wraps POST /api/v2/ads/campaigns/{id}/pause.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"action": {
"default": "pause",
"description": "stop = a pause you mean as final; the network shows paused either way.",
"enum": [
"pause",
"stop"
],
"type": "string"
},
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"id": {
"type": "string"
},
"idempotencyKey": {
"description": "Retrying with the same key never runs the change twice.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "ads_campaign_pause"
},
{
"description": "[read-only] The brand's ad campaigns with status, budget, start/end dates, max spend, spend to date, ad sets and ads. Filter by status or platform.\n(Wraps GET /api/v2/ads/campaigns.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"platform": {
"description": "e.g. facebook, instagram",
"type": "string"
},
"status": {
"description": "e.g. active, paused",
"type": "string"
}
},
"type": "object"
},
"name": "ads_campaigns_list"
},
{
"description": "[writes · `ads:spend` scope] Sets the brand's monthly and/or per-campaign activation caps, in cents (e.g. 50000 = $500). Inside the owner's agent ceiling it is applied at once; above it the result is `approval_requested`: tell the user the owner has an approval request in Ads > Agent activity, and repeat the same call once they approve. Pass agentName and reason; both are shown to the owner.\n(Wraps POST /api/v2/ads/caps.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"agentName": {
"description": "Which agent is acting. Shown to the owner in Ads > Agent activity.",
"type": "string"
},
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand.",
"type": "string"
},
"monthlyMaxMinor": {
"description": "Monthly activation cap, whole cents (100 to 10000000).",
"type": "integer"
},
"perCampaignMaxMinor": {
"description": "Per-campaign activation cap, whole cents (100 to 10000000).",
"type": "integer"
},
"reason": {
"description": "Why the agent is doing this. Shown to the owner and kept in the audit trail.",
"type": "string"
}
},
"required": [
"agentName",
"reason"
],
"type": "object"
},
"name": "ads_caps_set"
},
{
"description": "[read-only] Compare an older and a newer saved run: each finding is new, resolved, worse, better or unchanged, with metric deltas.\n(Wraps GET /api/v2/ads/deep-dives/compare.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"after": {
"description": "The newer run id.",
"type": "string"
},
"before": {
"description": "The older run id.",
"type": "string"
},
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand.",
"type": "string"
}
},
"required": [
"before",
"after"
],
"type": "object"
},
"name": "ads_deep_dive_compare"
},
{
"description": "[read-only] One saved deep dive by id, as json or an export (markdown, csv, pdf) of the stored report. List recent runs with GET /api/v2/ads/deep-dives.\n(Wraps GET /api/v2/ads/deep-dives/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand.",
"type": "string"
},
"format": {
"default": "json",
"enum": [
"json",
"markdown",
"csv",
"pdf"
],
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "ads_deep_dive_get"
},
{
"description": "[read-only] The Embers price of a deep dive on a saved snapshot, and whether a recent run makes it free. Charges nothing. Ask the owner before running.\n(Wraps GET /api/v2/ads/deep-dives/quote.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand.",
"type": "string"
},
"snapshotId": {
"description": "A saved ads data snapshot id.",
"type": "string"
}
},
"required": [
"snapshotId"
],
"type": "object"
},
"name": "ads_deep_dive_quote"
},
{
"description": "[writes flayr data · `write` scope] Run a deep dive on a saved snapshot and save the report. Charges Embers only when confirmEmbers equals the current quote total; a failed run is refunded. Changes no ad, budget or campaign.\n(Wraps POST /api/v2/ads/deep-dives.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand.",
"type": "string"
},
"confirmEmbers": {
"description": "The total Embers from the quote you agreed to.",
"minimum": 0,
"type": "integer"
},
"snapshotId": {
"type": "string"
}
},
"required": [
"snapshotId",
"confirmEmbers"
],
"type": "object"
},
"name": "ads_deep_dive_run"
},
{
"description": "[read-only] Impressions, clicks, spend, link clicks and leads per campaign for a date range (default the last 30 days, at most 93), with totals per currency. Pass `level: \"ad\"` for daily per-ad results summed per ad (spend, impressions, clicks, CTR, video views), each with the Library `contentId` the ad was built from (null when unlinked; `unlinked` counts those).\n(Wraps GET /api/v2/ads/insights.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"from": {
"description": "YYYY-MM-DD; default 30 days ago",
"type": "string"
},
"level": {
"default": "campaign",
"enum": [
"campaign",
"ad"
],
"type": "string"
},
"to": {
"description": "YYYY-MM-DD; default today",
"type": "string"
}
},
"type": "object"
},
"name": "ads_insights"
},
{
"description": "[writes flayr data · `write` scope] Records the claim of one offer row (`claimId` = a row id from ads_offers_list): status, the date it was claimed, and notes. Owners and admins of the brand only. A record only: it never creates, turns on or funds an ad.\n(Wraps PATCH /api/v2/ads/offers/{claimId}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands). Required for an account-wide key.",
"type": "string"
},
"claimId": {
"type": "string"
},
"claimedAt": {
"description": "YYYY-MM-DD (or ISO timestamp); null clears it.",
"type": [
"string",
"null"
]
},
"notes": {
"maxLength": 1000,
"type": [
"string",
"null"
]
},
"status": {
"enum": [
"not_claimed",
"claimed",
"earned",
"expired",
"ineligible"
],
"type": "string"
}
},
"required": [
"claimId"
],
"type": "object"
},
"name": "ads_offer_update"
},
{
"description": "[read-only] The brand's new-advertiser offers per ad network, one row per ad account (or a network not connected yet): status, spend so far, what is left to spend, the daily pace and the deadline, and how to claim. Read-only.\n(Wraps GET /api/v2/ads/offers.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands). Required for an account-wide key; a brand-bound key is pinned to its brand and any other value is 403.",
"type": "string"
}
},
"type": "object"
},
"name": "ads_offers_list"
},
{
"description": "[read-only] Whether each connected network and ad account can run ads, and what to fix first.\n(Wraps GET /api/v2/ads/readiness.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
}
},
"type": "object"
},
"name": "ads_readiness"
},
{
"description": "[writes flayr data · `write` scope] Save an immutable, org and brand scoped snapshot for selected ad accounts and an inclusive 1–30 day window. Reads vendors only; no charge, analysis, ad writes or spend. Each source reports availability.\n(Wraps POST /api/v2/ads/snapshots.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"adAccountIds": {
"items": {
"type": "string"
},
"maxItems": 10,
"minItems": 1,
"type": "array"
},
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"fromDate": {
"format": "date",
"type": "string"
},
"toDate": {
"format": "date",
"type": "string"
}
},
"required": [
"fromDate",
"toDate",
"adAccountIds"
],
"type": "object"
},
"name": "ads_snapshot_collect"
},
{
"description": "[read-only] Read one saved ads data snapshot by id, or recent snapshot headers for the active brand. No cross-org access.\n(Wraps GET /api/v2/ads/snapshots.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"id": {
"type": "string"
}
},
"type": "object"
},
"name": "ads_snapshots_get"
},
{
"description": "[writes flayr data · `write` scope] Draws a frame over an image and returns a new image URL (the original is untouched). Use the URL in create_content, or pass contentId + platform so that post publishes the framed image on that platform. Free by default; the same inputs again are free.\n(Wraps POST /api/v2/frames/apply.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"aspect": {
"description": "Default: nearest to the source image.",
"enum": [
"1:1",
"4:5",
"9:16",
"16:9"
],
"type": "string"
},
"contentId": {
"description": "A content id: its publish swaps this framed image in for `platform`.",
"type": "string"
},
"fit": {
"enum": [
"cover",
"contain"
],
"type": "string"
},
"frameRef": {
"description": "`default:<key>` or `brand:<id>` from GET /api/v2/frames.",
"type": "string"
},
"offsetX": {
"maximum": 1,
"minimum": -1,
"type": "number"
},
"offsetY": {
"maximum": 1,
"minimum": -1,
"type": "number"
},
"paletteOverride": {
"description": "Colors that win over the brand kit.",
"properties": {
"accent": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
},
"ink": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
},
"onPrimary": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
},
"paper": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
},
"primary": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
},
"secondary": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
}
},
"type": "object"
},
"platform": {
"description": "Platform this framed image is for (\"all\" by default).",
"type": "string"
},
"slideIndex": {
"description": "Image position in the post.",
"minimum": 0,
"type": "integer"
},
"sourceUrl": {
"description": "https image URL (flayr/Convex storage or a stock host).",
"type": "string"
},
"texts": {
"description": "Texts some frames print (ribbon/tag label, lower-third title/subtitle).",
"properties": {
"label": {
"maxLength": 60,
"type": "string"
},
"subtitle": {
"maxLength": 60,
"type": "string"
},
"title": {
"maxLength": 60,
"type": "string"
}
},
"type": "object"
},
"zoom": {
"maximum": 2,
"minimum": 1,
"type": "number"
}
},
"required": [
"sourceUrl",
"frameRef"
],
"type": "object"
},
"name": "apply_frame"
},
{
"description": "[writes flayr data · `write` scope] Maps library clips, Shopify products, features or texts to draft bodies (what each shows and proves) so hooks can lead into them. Charges Embers once per call (refunded when nothing usable comes back). Needs the Pro plan or higher.\n(Wraps POST /api/v2/hook-studio/bodies/map.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"features": {
"items": {
"type": "string"
},
"type": "array"
},
"includeCollab": {
"type": "boolean"
},
"libraryContentIds": {
"items": {
"type": "string"
},
"type": "array"
},
"productGids": {
"items": {
"type": "string"
},
"type": "array"
},
"texts": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"name": "bodies_map"
},
{
"description": "[read-only] A brand's legal name, registered address, website, currency and timezone: what ad networks ask for when you open an ads account. `hasTaxId` says whether a tax id is on file; the tax id itself is never returned. Use an `id` from list_brands.\n(Wraps GET /api/v2/brands/{id}/business-details.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"description": "A brand id from GET /api/v2/brands. A brand-bound key may only name its own brand (another brand → 403).",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "brand_business_details_get"
},
{
"description": "[writes flayr data · `write` scope] Sets a brand's legal name, registered address, currency (ISO, like USD) and/or timezone (IANA, like America/New_York). Only the fields sent change; a brand with no details yet needs all four. Do not send a tax id: it is refused. A brand owner or admin adds it in flayr (Brand › Overview).\n(Wraps PATCH /api/v2/brands/{id}/business-details.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"currency": {
"description": "3-letter ISO currency code.",
"pattern": "^[A-Za-z]{3}$",
"type": "string"
},
"id": {
"description": "A brand id from GET /api/v2/brands. A brand-bound key may only name its own brand (another brand → 403).",
"type": "string"
},
"legalName": {
"maxLength": 200,
"type": "string"
},
"registeredAddress": {
"description": "Replaced as a whole when sent.",
"properties": {
"city": {
"maxLength": 200,
"type": "string"
},
"country": {
"description": "2-letter ISO country code.",
"pattern": "^[A-Za-z]{2}$",
"type": "string"
},
"line1": {
"maxLength": 200,
"type": "string"
},
"line2": {
"maxLength": 200,
"type": "string"
},
"postalCode": {
"maxLength": 20,
"type": "string"
},
"region": {
"description": "State, province or county.",
"maxLength": 200,
"type": "string"
}
},
"required": [
"line1",
"city",
"region",
"postalCode",
"country"
],
"type": "object"
},
"timezone": {
"description": "IANA time zone.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "brand_business_details_update"
},
{
"description": "[read-only] A brand's profile (website, companyName, tagline, industry, audience, values, pillars, hashtag sets), its default voice, colors and fonts. `brandId` comes from list_brands; a brand-bound connection may omit it.\n(Wraps GET /api/v2/brand/intelligence.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. A brand-bound key is pinned to its own brand (may be omitted; another brand → 403); an account-wide key must name one.",
"type": "string"
}
},
"type": "object"
},
"name": "brand_profile_get"
},
{
"description": "[writes flayr data · `write` scope] Fills in a brand's profile — e.g. `{ brandId, profile: { website: \"https://…\" } }`. Changes only the fields you send; the voice, colors and fonts stay as they are unless you pass them. Use it to clear list_brands setup items such as hasWebsite. Owners, admins and editors only; every change is audited against this connection.\n(Wraps PATCH /api/v2/brand/intelligence.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. A brand-bound key is pinned to its own brand (may be omitted; another brand → 403); an account-wide key must name one.",
"type": "string"
},
"colors": {
"description": "REPLACES the brand palette, the same as saving Brand → Overview → Brand Colors: the stored list is primary, secondary, then palette (duplicates dropped); primary and secondary are its top two. 6-digit hex, `#` optional, stored lower case.",
"properties": {
"palette": {
"description": "Further brand colors, in order.",
"items": {
"pattern": "^#?[0-9a-fA-F]{6}$",
"type": "string"
},
"maxItems": 16,
"type": "array"
},
"primary": {
"pattern": "^#?[0-9a-fA-F]{6}$",
"type": "string"
},
"secondary": {
"pattern": "^#?[0-9a-fA-F]{6}$",
"type": "string"
}
},
"required": [
"primary"
],
"type": "object"
},
"fonts": {
"description": "Font family names. A role left out keeps its stored font. At least one is required.",
"properties": {
"body": {
"maxLength": 100,
"type": "string"
},
"heading": {
"maxLength": 100,
"type": "string"
}
},
"type": "object"
},
"profile": {
"description": "Only the fields sent are changed. Unknown fields are a 422.",
"properties": {
"brandValues": {
"items": {
"type": "string"
},
"type": "array"
},
"companyName": {
"maxLength": 200,
"type": "string"
},
"competitors": {
"items": {
"type": "string"
},
"type": "array"
},
"contentPillars": {
"items": {
"type": "string"
},
"type": "array"
},
"hashtagSets": {
"items": {
"properties": {
"name": {
"type": "string"
},
"tags": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"name",
"tags"
],
"type": "object"
},
"maxItems": 20,
"type": "array"
},
"industry": {
"maxLength": 200,
"type": "string"
},
"mission": {
"maxLength": 4000,
"type": "string"
},
"tagline": {
"maxLength": 200,
"type": "string"
},
"targetDemographics": {
"properties": {
"ageRanges": {
"items": {
"type": "string"
},
"type": "array"
},
"gender": {
"type": "string"
},
"interests": {
"items": {
"type": "string"
},
"type": "array"
},
"location": {
"items": {
"type": "string"
},
"type": "array"
},
"painPoints": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"website": {
"maxLength": 200,
"type": "string"
}
},
"type": "object"
},
"voice": {
"description": "The brand default voice. Created (as the default) when the brand has none.",
"properties": {
"avoidWords": {
"items": {
"type": "string"
},
"type": "array"
},
"exampleLinks": {
"items": {
"type": "string"
},
"type": "array"
},
"examplePosts": {
"items": {
"type": "string"
},
"type": "array"
},
"guide": {
"maxLength": 30000,
"type": "string"
},
"keywords": {
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},
"sampleContent": {
"type": "string"
},
"style": {
"type": "string"
},
"targetAudience": {
"type": "string"
},
"tone": {
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
},
"name": "brand_profile_update"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] Takes a SCHEDULED post off the platform schedule and marks it cancelled (the draft stays, so it can be published again). Safe to repeat. Refused within 5 minutes of the slot, and when the platform will not remove it (some YouTube uploads). Needs the `publish` scope (or admin) and the Pro plan or higher; each publish counts against the plan's direct posts for the month. Confirm with the user first.\n(Wraps DELETE /api/v2/posts/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"description": "The post: `convexPostId` from POST /api/v2/publish, or `posts[].id` from GET /api/v2/content/{id}.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "cancel_scheduled_post"
},
{
"description": "[read-only] Checks a hook, CTA, caption or ad against the brand's claims sources (verified prices, banned phrases, competitors, destinations). No model call, nothing stored, free. Needs the Pro plan or higher.\n(Wraps POST /api/v2/hook-studio/claims/check.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"context": {
"enum": [
"hook",
"cta",
"caption",
"ad"
],
"type": "string"
},
"destination": {
"type": "string"
},
"platforms": {
"items": {
"type": "string"
},
"type": "array"
},
"text": {
"maxLength": 5000,
"type": "string"
}
},
"required": [
"text",
"context"
],
"type": "object"
},
"name": "claims_check"
},
{
"description": "[writes flayr data · `write` scope] Freezes the open round's numbers and keeps the best arms for the next round. A winner is recorded only when the readout says likely_winner, or when `pick` names an arm the user chose. Ask the user before passing `pick`.\n(Wraps POST /api/v2/experiments/{id}/close-round.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"pick": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "close_experiment_round"
},
{
"description": "[writes flayr data · `write` scope] Extends a video draft (`id`) with more clips and image cards into ONE finished video, rendered by flayr and saved as a NEW linked draft (same brand, caption copied, credits merged, labelled variant:combined). Nothing is posted and the original is unchanged. Use it to show a finished print with painting/assembly clips before or after it. `segments` (2-8, in order) must include the video itself as { kind: \"clip\", contentId: id }; clips are Library videos (contentId) or uploads (storageId from upload_media) with inMs/outMs (or durationMs = the clip's full length); cards are images (contentId of a Library image or video thumbnail, or imageStorageId) with durationMs and optional text. `transition` cut|crossfade, `audio` keep|mute_extras|main_across. Max 60 min by default (long-form: cold open + trimmed recording + bumpers). Costs 25 Embers per started minute of output for the first 3 min, then 5 per further started minute by default (refunded on failure). Returns a jobId — poll get_job_status.\n(Wraps POST /api/v2/content/{id}/combine.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"audio": {
"default": "keep",
"enum": [
"keep",
"mute_extras",
"main_across"
],
"type": "string"
},
"crossfadeMs": {
"default": 400,
"description": "Crossfade overlap per join.",
"maximum": 500,
"minimum": 300,
"type": "integer"
},
"id": {
"description": "The video to extend.",
"type": "string"
},
"segments": {
"description": "In playback order. Must include this video ({ \"kind\": \"clip\", \"contentId\": \"<id>\" }).",
"items": {
"additionalProperties": false,
"properties": {
"contentId": {
"description": "clip: a Library video. card: a Library image, or a video (its thumbnail).",
"type": "string"
},
"durationMs": {
"description": "clip: the source clip's full length (needed when outMs is omitted). card: how long it shows (1000-10000).",
"minimum": 1,
"type": "integer"
},
"imageStorageId": {
"description": "card only: an uploaded image.",
"type": "string"
},
"inMs": {
"description": "clip only: where the clip starts (default 0).",
"minimum": 0,
"type": "integer"
},
"kind": {
"enum": [
"clip",
"card"
],
"type": "string"
},
"outMs": {
"description": "clip only: where the clip ends. Defaults to durationMs.",
"minimum": 0,
"type": "integer"
},
"storageId": {
"description": "clip only: an uploaded video (POST /api/v2/media/upload-url).",
"type": "string"
},
"text": {
"description": "card only: one short line over the image.",
"maxLength": 80,
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
"maxItems": 8,
"minItems": 2,
"type": "array"
},
"transition": {
"default": "cut",
"enum": [
"cut",
"crossfade"
],
"type": "string"
}
},
"required": [
"id",
"segments"
],
"type": "object"
},
"name": "combine_clips"
},
{
"description": "[read-only] Splits every labelled post by the values of one label key (e.g. key \"cut\" → cut:opus vs cut:raw) and reports medians with n per group at the 48h / 7d snapshot. Descriptive, not a significance test. Pro plan or higher.\n(Wraps GET /api/v2/analytics/experiments.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id, or \"all\" (default: every profile you can see). A brand-bound key is always its own brand.",
"type": "string"
},
"include": {
"enum": [
"posts"
],
"type": "string"
},
"key": {
"default": "cut",
"type": "string"
},
"metric": {
"default": "impressions",
"description": "Which metric \"leading\" ranks by.",
"enum": [
"impressions",
"views",
"engagement",
"engagementRate",
"clicks"
],
"type": "string"
},
"window": {
"default": "48h",
"enum": [
"48h",
"7d",
"latest"
],
"type": "string"
}
},
"type": "object"
},
"name": "compare_labels"
},
{
"description": "[read-only] Per-account metrics and tag, campaign, experiment and series roll-ups. Omit postId for scope-wide roll-ups (up to 100 posts). Include reruns to compare bookings of the same content over time. Historical data stays platform-attributed. Pro plan or higher.\n(Wraps GET /api/v2/analytics/instances.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "Brand id or all; brand-bound keys remain pinned.",
"type": "string"
},
"includeReruns": {
"default": false,
"description": "Compare all readable bookings of the same contentId.",
"type": "boolean"
},
"postId": {
"description": "Local Flayr post id; omit for roll-ups across the readable scope (up to 100 posts).",
"type": "string"
}
},
"type": "object"
},
"name": "compare_post_instances"
},
{
"description": "[writes flayr data · `write` scope] Creates a DRAFT content item. Nothing is posted. Give `text` plus optional media (`videoUrl` / `imageUrl`, or `videoStorageId` / `imageStorageId` from upload_media), target `platforms`, `brandId` (from list_brands; omitted → the brand last active in the dashboard), per-platform captions (`platformText`), creator `credits`, `labels` and YouTube options. Returns the new `id` — pass it as `contentId` to publish or add_queue_items.\n(Wraps POST /api/v2/content.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "An id from GET /api/v2/brands, or \"personal\".",
"type": "string"
},
"contentType": {
"type": "string"
},
"copyFromContentId": {
"description": "COPY TO BRAND (staff and the Agency plan): the id of an existing item (GET /api/v2/content) to copy into `brandId` as a new draft. Images, video, slides, documents and thumbnails are copied into new storage objects, so deleting the source never breaks the copy. Combine only with `brandId` (required, a brand id — not \"personal\") and optionally `text` (replaces the caption; omitted = the source caption). Labels, mentions, campaign/experiment links and post history are not copied. Needs an account-wide key. Errors: 403 below the Agency plan or with a brand-bound key; 404 unknown item or brand; 409 the item changed mid-copy; 413 the brand's storage is full; 422 the item is already in that brand or its media is missing.",
"type": "string"
},
"credits": {
"description": "Creator credits for work this post uses. Added to the published caption above the flayr footer, one line each: model → \"Print your own: <url> (model by <author>)\"; other kinds → \"Credit: <title> by <author> <url>\". Never added twice (a URL already in the caption is skipped); on X a shorter line (or no URL) is used, or the credits are left out, before the caption text is ever cut. YouTube: description only, never the title.",
"items": {
"additionalProperties": false,
"properties": {
"author": {
"description": "Designer / artist / photographer.",
"maxLength": 200,
"type": "string"
},
"kind": {
"enum": [
"model",
"music",
"footage",
"image",
"other"
],
"type": "string"
},
"site": {
"description": "Detected from the URL when omitted (e.g. MakerWorld, Printables, Thingiverse).",
"maxLength": 200,
"type": "string"
},
"title": {
"maxLength": 200,
"type": "string"
},
"url": {
"description": "Absolute http(s) link to the credited work.",
"format": "uri",
"maxLength": 2048,
"type": "string"
}
},
"required": [
"kind",
"url"
],
"type": "object"
},
"maxItems": 10,
"type": "array"
},
"imageStorageId": {
"type": "string"
},
"imageUrl": {
"type": "string"
},
"instanceLabels": {
"description": "Internal labels per platform/account instance. An array of {platform, accountId?, labels}; accountId is the Zernio connected account id. Omit accountId when unknown. instanceKey is derived as canonical platform + \":account:\" + URL-encoded account id, or \":pending\" when unknown (twitter canonicalizes to x). Replaces the instance set when editing; [] clears it. Never published.",
"items": {
"additionalProperties": false,
"properties": {
"accountId": {
"maxLength": 200,
"type": "string"
},
"labels": {
"items": {
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"platform": {
"type": "string"
}
},
"required": [
"platform",
"labels"
],
"type": "object"
},
"maxItems": 20,
"type": "array"
},
"labels": {
"description": "Normalized \"key:value\" labels (keys and values are lower-cased, spaces become \"-\"). Linked keys: campaign:<media campaign id>, experiment:<experiment id> (+ variant:<name> or the experiment's compareKey), type:<post type> (short, long-form, carousel, image, text, reel, story, poem, timelapse…). Free keys work too: cut:opus, lane:tech, parent:pantheon-part-1, ab:<pair id> (an A/B pair). Labels are copied onto every post made from the draft, and compared in GET /api/v2/analytics/experiments.",
"items": {
"pattern": "^[a-z0-9][a-z0-9_-]{0,31}:[a-z0-9][a-z0-9._\\-/+]{0,79}$",
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"platformText": {
"additionalProperties": {
"maxLength": 10000,
"type": [
"string",
"null"
]
},
"description": "Per-platform captions: platform id → the caption that platform gets instead of `text`, e.g. { \"twitter\": \"short version\" }. Each must fit its platform: X 280 (a link counts 23, an emoji 2), Bluesky 300, Threads 500, Instagram 2200, LinkedIn 3000. Platforms without one get `text` when it fits, else an automatic short version: the first line, the credits in short form, as many hashtags as fit and the short flayr link (hashtags go first, then the credit link becomes \"Model by <author>\"; words are never cut). PATCH merges: a null (or \"\") value clears that platform, platformText: null clears all. At publish every platform is validated on its own caption: one failing platform no longer fails the others — the post publishes to the rest and reports the failures in platformErrors.",
"type": "object"
},
"platforms": {
"items": {
"type": "string"
},
"type": "array"
},
"referralFooter": {
"description": "Caption watermark: appends \"Posted with flayr.social — try it: flayr.social/r/<code>/<ch>\" to each platform's text (<code> is your referral code — your custom one if you claimed it; <ch> names the platform: yt, x, li, ig, tt, fb, th, bs). (YouTube: description only, never the title; X: the shorter \"via flayr.social/r/<code>/x\" or nothing when it would not fit — your text is never cut). On by default. false removes it only on plans that can remove the Flayr watermark (Pro and up); omitted follows your watermark setting.",
"type": "boolean"
},
"sourceLabel": {
"maxLength": 80,
"type": "string"
},
"text": {
"maxLength": 10000,
"minLength": 1,
"type": "string"
},
"videoStorageId": {
"type": "string"
},
"videoUrl": {
"type": "string"
},
"youtube": {
"description": "Stored on the draft and used when it is published (POST /api/v2/publish with contentId). YouTube upload options, applied to YouTube targets only. madeForKids is always sent to YouTube (default false). A missing title is derived from the first sentence of the caption, cut to 100 characters at a word boundary. SCHEDULED UPLOADS: the video is uploaded ahead of scheduledAt. \"public\" is uploaded as private and released by YouTube at the scheduled time; \"unlisted\" is watchable by anyone with the link from upload time; \"private\" stays private.",
"properties": {
"categoryId": {
"description": "YouTube category id, e.g. \"28\" (Science & Technology). YouTube default: 22.",
"pattern": "^\\d{1,3}$",
"type": "string"
},
"containsSyntheticMedia": {
"description": "AI-generated content disclosure.",
"type": "boolean"
},
"firstComment": {
"description": "For scheduled videos, posted at the scheduled time.",
"maxLength": 10000,
"type": "string"
},
"madeForKids": {
"default": false,
"type": "boolean"
},
"playlistId": {
"description": "From GET /api/v2/youtube/playlists for the same brand.",
"type": "string"
},
"tags": {
"description": "At most 500 characters combined.",
"items": {
"maxLength": 100,
"type": "string"
},
"type": "array"
},
"title": {
"description": "No < or >.",
"maxLength": 100,
"minLength": 1,
"type": "string"
},
"visibility": {
"default": "public",
"enum": [
"public",
"private",
"unlisted"
],
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
},
"name": "create_content"
},
{
"description": "[writes flayr data · `write` scope] Creates an A/B experiment. Then tag drafts with its `label` (experiment:<id>) and `<compareKey>:<variant>` via update_content `labels`, publish them, and read get_experiment_results.\n(Wraps POST /api/v2/experiments.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id, or \"personal\" (default).",
"type": "string"
},
"compareKey": {
"type": "string"
},
"endAt": {
"format": "date-time",
"type": [
"string",
"null"
]
},
"hypothesis": {
"maxLength": 2000,
"type": "string"
},
"name": {
"maxLength": 120,
"type": "string"
},
"notes": {
"maxLength": 2000,
"type": "string"
},
"primaryMetric": {
"enum": [
"impressions",
"views",
"engagement",
"engagementRate",
"clicks"
],
"type": "string"
},
"startAt": {
"format": "date-time",
"type": [
"string",
"null"
]
},
"status": {
"enum": [
"draft",
"running",
"done"
],
"type": "string"
},
"variants": {
"items": {
"type": "string"
},
"maxItems": 12,
"type": "array"
}
},
"required": [
"name"
],
"type": "object"
},
"name": "create_experiment"
},
{
"description": "[writes flayr data · `write` scope] Copy a flayr frame into the brand library (`defaultKey`, optional colors/texts), or add uploaded PNG/SVG art (upload_media first; `variants` of storageIds, the content area is detected from transparency).\n(Wraps POST /api/v2/frames.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"defaultKey": {
"description": "A defaults[].ref without \"default:\".",
"type": "string"
},
"kind": {
"description": "Default: guessed from the name.",
"enum": [
"border",
"overlay",
"lower_third",
"corner_bug",
"quote_card",
"ribbon"
],
"type": "string"
},
"name": {
"maxLength": 80,
"type": "string"
},
"paletteOverride": {
"description": "Colors that win over the brand kit.",
"properties": {
"accent": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
},
"ink": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
},
"onPrimary": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
},
"paper": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
},
"primary": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
},
"secondary": {
"pattern": "^#[0-9a-fA-F]{6}$",
"type": "string"
}
},
"type": "object"
},
"rights": {
"description": "Who holds the rights to the art. Optional; a frame without it shows a reminder.",
"enum": [
"original",
"owned",
"licensed"
],
"type": "string"
},
"tags": {
"items": {
"type": "string"
},
"maxItems": 20,
"type": "array"
},
"texts": {
"description": "Texts some frames print (ribbon/tag label, lower-third title/subtitle).",
"properties": {
"label": {
"maxLength": 60,
"type": "string"
},
"subtitle": {
"maxLength": 60,
"type": "string"
},
"title": {
"maxLength": 60,
"type": "string"
}
},
"type": "object"
},
"variants": {
"description": "One per aspect ratio.",
"items": {
"properties": {
"aspect": {
"description": "Default: nearest to the image size.",
"enum": [
"1:1",
"4:5",
"9:16",
"16:9"
],
"type": "string"
},
"contentBox": {
"description": "Default: detected from the transparent area. Where the post sits inside the frame, as fractions (0–1) of the frame canvas.",
"properties": {
"h": {
"type": "number"
},
"w": {
"type": "number"
},
"x": {
"type": "number"
},
"y": {
"type": "number"
}
},
"required": [
"x",
"y",
"w",
"h"
],
"type": "object"
},
"fit": {
"enum": [
"cover",
"contain"
],
"type": "string"
},
"storageId": {
"description": "From POST /api/v2/media/upload-url, then POST the PNG (with transparency) or SVG bytes.",
"type": "string"
}
},
"required": [
"storageId"
],
"type": "object"
},
"maxItems": 4,
"minItems": 1,
"type": "array"
}
},
"type": "object"
},
"name": "create_frame"
},
{
"description": "[writes flayr data · `write` scope] Creates a keyword trigger, always INACTIVE: nothing is sent until a person turns it on. DMs work on Instagram and Facebook only; the reply text must not contain a keyword.\n(Wraps POST /api/v2/growth-triggers.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"accountId": {
"description": "A connected account id (GET /api/v2/accounts); omitted = every account of the brand.",
"type": "string"
},
"brandId": {
"type": "string"
},
"campaignKey": {
"description": "utm_campaign for {{link}}, and the campaign column of the logs.",
"type": "string"
},
"delaySeconds": {
"description": "Wait before replying; capped at 20 at send time.",
"minimum": 0,
"type": "number"
},
"dmText": {
"description": "Private reply (Instagram and Facebook only; refused on an account of another platform).",
"type": "string"
},
"dmVariations": {
"items": {
"type": "string"
},
"type": "array"
},
"keywords": {
"description": "Case-insensitive. exact = the whole comment; startsWith = its start; contains = anywhere (substring, so \"hi\" also matches \"this\").",
"items": {
"properties": {
"matchType": {
"enum": [
"exact",
"contains",
"startsWith"
],
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"value",
"matchType"
],
"type": "object"
},
"minItems": 1,
"type": "array"
},
"link": {
"description": "http(s) URL behind {{link}}.",
"format": "uri",
"type": "string"
},
"maxSends": {
"description": "After this many sends, reply only overCapText.",
"minimum": 1,
"type": "integer"
},
"name": {
"type": "string"
},
"overCapText": {
"type": "string"
},
"postIds": {
"description": "Only comments on these posts; omitted = all posts.",
"items": {
"type": "string"
},
"type": "array"
},
"priority": {
"description": "Higher is checked first; the first matching trigger wins.",
"type": "number"
},
"repeatPolicy": {
"default": "once_per_commenter",
"enum": [
"once_per_commenter",
"every_comment"
],
"type": "string"
},
"replyText": {
"description": "Public reply. {{name}} = the commenter, {{link}} = `link` with UTM tags. Must not contain a keyword (it would trigger itself).",
"type": "string"
},
"replyVariations": {
"description": "Alternatives picked at random with replyText.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"name",
"keywords"
],
"type": "object"
},
"name": "create_growth_trigger"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] Creates a posting queue. Once items are added it BOOKS REAL POSTS on the given platforms on its cadence (default daily 19:00 America/Chicago, 2 booked ahead). Use `startsAt` to hold everything until a date. Needs the `publish` scope (or admin) on the Agency plan or higher.\n(Wraps POST /api/v2/queues.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"type": "string"
},
"cadenceHours": {
"default": 24,
"description": "1, 2, 3, 4, 6, 8, 12, or whole days (24, 48, …, 720).",
"type": "integer"
},
"lookahead": {
"default": 2,
"description": "How many upcoming items are scheduled at the platform at once.",
"maximum": 10,
"minimum": 1,
"type": "integer"
},
"name": {
"maxLength": 80,
"type": "string"
},
"platforms": {
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"startsAt": {
"description": "ISO 8601 with an offset (or Z). Nothing is booked before it; the first slot is the first timeOfDay (in the queue's timezone) at or after it, so a startsAt that is itself a slot is the first slot. A past date holds nothing back.",
"format": "date-time",
"type": "string"
},
"timeOfDay": {
"default": "19:00",
"description": "24-hour \"HH:MM\" local time.",
"type": "string"
},
"timezone": {
"default": "America/Chicago",
"description": "IANA timezone. Slots keep their local time across DST.",
"type": "string"
},
"youtube": {
"description": "Queue-wide YouTube defaults (a draft's own options win; titles are per item). YouTube upload options, applied to YouTube targets only. madeForKids is always sent to YouTube (default false). A missing title is derived from the first sentence of the caption, cut to 100 characters at a word boundary. SCHEDULED UPLOADS: the video is uploaded ahead of scheduledAt. \"public\" is uploaded as private and released by YouTube at the scheduled time; \"unlisted\" is watchable by anyone with the link from upload time; \"private\" stays private.",
"properties": {
"categoryId": {
"description": "YouTube category id, e.g. \"28\" (Science & Technology). YouTube default: 22.",
"pattern": "^\\d{1,3}$",
"type": "string"
},
"containsSyntheticMedia": {
"description": "AI-generated content disclosure.",
"type": "boolean"
},
"firstComment": {
"description": "For scheduled videos, posted at the scheduled time.",
"maxLength": 10000,
"type": "string"
},
"madeForKids": {
"default": false,
"type": "boolean"
},
"playlistId": {
"description": "From GET /api/v2/youtube/playlists for the same brand.",
"type": "string"
},
"tags": {
"description": "At most 500 characters combined.",
"items": {
"maxLength": 100,
"type": "string"
},
"type": "array"
},
"title": {
"description": "No < or >.",
"maxLength": 100,
"minLength": 1,
"type": "string"
},
"visibility": {
"default": "public",
"enum": [
"public",
"private",
"unlisted"
],
"type": "string"
}
},
"type": "object"
}
},
"required": [
"name",
"platforms"
],
"type": "object"
},
"name": "create_queue"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] Creates a rule that reshares (or comments on) every new post of the source accounts. Defaults to needs_approval: each fire waits for approve in act_on_publish_action. Only LinkedIn (native reshare) and X (quote own / retweet) really repost; other platforms get a link share. Confirm with the user first. Needs the `publish` scope (or admin) on the Agency plan or higher.\n(Wraps POST /api/v2/automations/reshare-rules.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"approval": {
"default": "needs_approval",
"description": "`needs_approval` (default): each fire waits for a person (POST …/actions/{id} `approve`). `auto`: runs on its own.",
"enum": [
"needs_approval",
"auto"
],
"type": "string"
},
"brandId": {
"type": "string"
},
"delayMinutes": {
"default": 0,
"minimum": 0,
"type": "integer"
},
"enabled": {
"default": true,
"type": "boolean"
},
"kind": {
"default": "reshare",
"description": "`reshare`: share the post to `targets`. `comment`: comment on the post itself through the source account.",
"enum": [
"reshare",
"comment"
],
"type": "string"
},
"name": {
"maxLength": 120,
"type": "string"
},
"sourceAccountIds": {
"description": "Fires when a post goes live through any of these connected accounts.",
"items": {
"type": "string"
},
"type": "array"
},
"sourceLabel": {
"description": "Only posts carrying this label.",
"type": [
"string",
"null"
]
},
"targets": {
"items": {
"properties": {
"accountId": {
"type": "string"
},
"platform": {
"type": "string"
}
},
"required": [
"accountId",
"platform"
],
"type": "object"
},
"type": "array"
},
"template": {
"description": "The reshare commentary or comment text. Tokens, resolved only once the URL exists: `{{postUrl}}` (this post, on the comment's platform), `{{postUrl:<platform>}}`, `{{parentPostUrl:<post or draft id>}}` (another flayr post: same platform, else YouTube, else any), `{{youtubeUrl:<post or draft id>}}`.",
"type": "string"
}
},
"required": [
"name",
"sourceAccountIds"
],
"type": "object"
},
"name": "create_share_rule"
},
{
"description": "[writes flayr data · `write` scope] Creates a reusable format (text template) in a brand.\n(Wraps POST /api/v2/text-templates.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"type": "string"
},
"ctaSlots": {
"items": {
"properties": {
"label": {
"type": "string"
},
"placement": {
"default": "body",
"enum": [
"body",
"first_comment",
"none"
],
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"label"
],
"type": "object"
},
"maxItems": 10,
"type": "array"
},
"defaultLabels": {
"description": "\"key:value\" labels every draft started from this format gets (plus the format's own `label`).",
"items": {
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"example": {
"maxLength": 20000,
"type": [
"string",
"null"
]
},
"format": {
"enum": [
"post",
"article",
"carousel_script",
"meme",
"thread"
],
"type": "string"
},
"guardrails": {
"items": {
"maxLength": 500,
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"hookPatterns": {
"description": "\"[slot]\" placeholders are kept as written.",
"items": {
"maxLength": 300,
"type": "string"
},
"maxItems": 20,
"type": "array"
},
"length": {
"properties": {
"captionWords": {
"description": "carousel_script: the caption that goes with the slides.",
"properties": {
"max": {
"type": "integer"
},
"min": {
"type": "integer"
}
},
"type": "object"
},
"max": {
"minimum": 1,
"type": "integer"
},
"min": {
"minimum": 0,
"type": "integer"
},
"perSlideMaxWords": {
"description": "carousel_script / thread: words allowed per slide or post.",
"type": "integer"
},
"titleMaxWords": {
"description": "carousel_script: words allowed in the title (first line of slide 1).",
"type": "integer"
},
"unit": {
"enum": [
"words",
"chars",
"slides"
],
"type": "string"
}
},
"required": [
"unit",
"min",
"max"
],
"type": "object"
},
"maxHashtags": {
"maximum": 30,
"minimum": 0,
"type": [
"integer",
"null"
]
},
"name": {
"maxLength": 120,
"type": "string"
},
"platform": {
"description": "e.g. \"linkedin\"; null = any platform.",
"type": [
"string",
"null"
]
},
"sections": {
"description": "In order. \"Start from a format\" writes one `[you fill: Label — guidance]` placeholder per section.",
"items": {
"properties": {
"guidance": {
"maxLength": 1000,
"type": "string"
},
"key": {
"description": "Derived from the label when omitted.",
"type": "string"
},
"label": {
"maxLength": 80,
"type": "string"
},
"maxLines": {
"type": "integer"
},
"minLines": {
"type": "integer"
},
"required": {
"default": true,
"type": "boolean"
}
},
"required": [
"label"
],
"type": "object"
},
"maxItems": 20,
"type": "array"
},
"source": {
"description": "Where the shape came from, e.g. an import path.",
"maxLength": 200,
"type": [
"string",
"null"
]
},
"useFor": {
"description": "One line: when to pick this format.",
"maxLength": 300,
"type": [
"string",
"null"
]
}
},
"required": [
"name",
"format",
"length"
],
"type": "object"
},
"name": "create_text_template"
},
{
"description": "[read-only] Checks whether a draft, post or link points at the brand's goal page (the same check the ad and boost routes run in paid mode; a block there refuses the ad). Run it before every boost or ad and paste the result. Nothing stored, free. Needs the Pro plan or higher.\n(Wraps POST /api/v2/cta-check.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"adButton": {
"description": "The ad button, e.g. LEARN_MORE.",
"type": "string"
},
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"caption": {
"description": "The caption, when it differs from the draft.",
"type": "string"
},
"contentId": {
"description": "A draft id.",
"type": "string"
},
"linkUrl": {
"description": "The link the ad or post would use.",
"type": "string"
},
"mode": {
"description": "Default: paid.",
"enum": [
"paid",
"organic"
],
"type": "string"
},
"objective": {
"description": "The ad objective, e.g. traffic, link_clicks, engagement. Click objectives can block; others warn.",
"type": "string"
},
"platforms": {
"items": {
"type": "string"
},
"type": "array"
},
"postId": {
"description": "A post id.",
"type": "string"
}
},
"type": "object"
},
"name": "cta_check"
},
{
"description": "[read-only] The brand's goals (source tagged, implicit from the website, or none; the primary goal) and its CTA check settings (ctaLevel, ctaPaidLevel, organicSupportingHosts, autoApplyPrimaryCta). Free. Needs the Pro plan or higher.\n(Wraps GET /api/v2/hook-studio/cta-settings.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
}
},
"type": "object"
},
"name": "cta_settings_get"
},
{
"description": "[writes flayr data · `write` scope] Changes only the CTA check settings sent: ctaLevel (off, warn, block), ctaPaidLevel (warn or block), organicSupportingHosts and autoApplyPrimaryCta. Only the brand's owners and admins can change them. Confirm changes with the person first. Free. Needs the Pro plan or higher.\n(Wraps PATCH /api/v2/hook-studio/cta-settings.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"autoApplyPrimaryCta": {
"description": "Studio adds the primary CTA to a post with none. Default: true.",
"type": "boolean"
},
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"ctaLevel": {
"description": "The CTA check on organic posts. Default: warn.",
"enum": [
"off",
"warn",
"block"
],
"type": "string"
},
"ctaPaidLevel": {
"description": "The CTA check on paid posts, never off. Default: block.",
"enum": [
"warn",
"block"
],
"type": "string"
},
"organicSupportingHosts": {
"description": "Hosts an organic post may link to besides a goal. Default: the website, youtube.com, youtu.be, github.com.",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"name": "cta_settings_update"
},
{
"description": "[writes flayr data · `write` scope] Adds a call to action written by the person, claims-checked, optionally tagged as a goal (goalKey; isPrimary makes it the brand's primary goal and demotes the previous one). Nothing here creates or runs an ad. Free. Needs the Pro plan or higher.\n(Wraps POST /api/v2/hook-studio/ctas.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"adButton": {
"description": "The ad button. Default: the ads config default CTA.",
"enum": [
"LEARN_MORE",
"SIGN_UP",
"BOOK_NOW",
"WATCH_MORE",
"SHOP_NOW",
"CONTACT_US",
"SUBSCRIBE",
"GET_OFFER",
"GET_QUOTE",
"APPLY_NOW",
"DOWNLOAD",
"ORDER_NOW"
],
"type": "string"
},
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"card": {
"type": "string"
},
"destination": {
"description": "An https link, or \"bio\" for the link in bio.",
"type": "string"
},
"goalKey": {
"description": "The goal this CTA serves, a short slug such as `tech-checkup`.",
"type": "string"
},
"isPrimary": {
"description": "The brand's primary goal. Setting it demotes the previous primary.",
"type": "boolean"
},
"line": {
"type": "string"
},
"offerId": {
"type": "string"
},
"paidEligible": {
"description": "Ads may use this CTA. Default: true for a primary, false otherwise.",
"type": "boolean"
},
"platforms": {
"items": {
"type": "string"
},
"type": "array"
},
"status": {
"enum": [
"draft",
"approved"
],
"type": "string"
}
},
"required": [
"card",
"line",
"destination",
"platforms"
],
"type": "object"
},
"name": "ctas_create"
},
{
"description": "[writes flayr data · `write` scope] Writes a batch of draft calls to action for the confirmed offers, claims-checked against where they point. Charges Embers once per call (refunded when nothing usable comes back). Needs the Pro plan or higher.\n(Wraps POST /api/v2/hook-studio/ctas/generate.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"count": {
"maximum": 20,
"minimum": 1,
"type": "integer"
},
"destination": {
"description": "An https link every CTA may point at, or `bio` for link in bio.",
"type": "string"
},
"offerIds": {
"items": {
"type": "string"
},
"type": "array"
},
"platforms": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"name": "ctas_generate"
},
{
"description": "[read-only] The brand's CTA bank (filter by status: draft, approved, retired), with each CTA's goal (goalKey, isPrimary, paidEligible, adButton) and claims result. Free. Needs the Pro plan or higher.\n(Wraps GET /api/v2/hook-studio/ctas.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"status": {
"enum": [
"draft",
"approved",
"retired"
],
"type": "string"
}
},
"type": "object"
},
"name": "ctas_list"
},
{
"description": "[writes flayr data · `write` scope] Edits a CTA, its status or its goal fields (goalKey, isPrimary, paidEligible, adButton). New copy or a new destination is claims-checked again; a CTA whose claims block cannot be approved. Free. Needs the Pro plan or higher.\n(Wraps PATCH /api/v2/hook-studio/ctas.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"adButton": {
"description": "The ad button. Default: the ads config default CTA.",
"enum": [
"LEARN_MORE",
"SIGN_UP",
"BOOK_NOW",
"WATCH_MORE",
"SHOP_NOW",
"CONTACT_US",
"SUBSCRIBE",
"GET_OFFER",
"GET_QUOTE",
"APPLY_NOW",
"DOWNLOAD",
"ORDER_NOW"
],
"type": "string"
},
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"card": {
"type": "string"
},
"destination": {
"description": "An https link, or \"bio\" for the link in bio.",
"type": "string"
},
"goalKey": {
"description": "The goal this CTA serves, a short slug such as `tech-checkup`.",
"type": "string"
},
"id": {
"type": "string"
},
"isPrimary": {
"description": "The brand's primary goal. Setting it demotes the previous primary.",
"type": "boolean"
},
"line": {
"type": "string"
},
"offerId": {
"type": "string"
},
"paidEligible": {
"description": "Ads may use this CTA. Default: true for a primary, false otherwise.",
"type": "boolean"
},
"platforms": {
"items": {
"type": "string"
},
"type": "array"
},
"status": {
"enum": [
"draft",
"approved",
"retired"
],
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "ctas_update"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] Same rules as the dashboard: the assigned approver, or an owner/admin of the org, may decide. On a \"workflow\" approval, decision \"approve\" resumes the parked run, whose next steps may publish (GOES LIVE); \"reject\" cancels it. On a \"manual\" approval, \"approve\" only marks the content approved — nothing is posted until you call publish with its contentId — and \"reject\" returns it to draft. On \"reject\", pass the reviewer's reason as `note` (the submitter is told it); it is optional unless the org requires one (then 422 without it). Needs the explicit `approve` scope (admin does not include it). Agency plan. Repeating the same decision is safe (alreadyDecided: true). Confirm with the user before approving a workflow approval.\n(Wraps POST /api/v2/approvals/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"decision": {
"enum": [
"approve",
"reject"
],
"type": "string"
},
"id": {
"description": "An approval id.",
"type": "string"
},
"note": {
"description": "On reject: the reason. Optional unless the org requires one.",
"maxLength": 1000,
"type": "string"
}
},
"required": [
"id",
"decision"
],
"type": "object"
},
"name": "decide_approval"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] Decides several variants of a group from list_approval_groups at once, each as decide_approval would: `members` \"all\" (open variants), an array of approvalIds, or {axis: value} (e.g. {\"audio\":\"voice\"}). \"approve\" marks them approved (a manual approval is NOT posted; a workflow approval resumes its run, which may publish — GOES LIVE); \"redo\" rejects with the Redo reason; \"drop\" rejects (`note` = reason). Already-decided variants are skipped. Needs the explicit `approve` scope (admin does not include it). Agency plan. Confirm the count with the user first.\n(Wraps POST /api/v2/approval-groups/{groupId}/decide.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"decision": {
"enum": [
"approve",
"redo",
"drop"
],
"type": "string"
},
"groupId": {
"description": "A groupId from GET /api/v2/approval-groups.",
"type": "string"
},
"members": {
"description": "\"all\" (default): every open variant. An array: those approval ids. An object: the variants whose labels match every axis value, e.g. {\"audio\":\"voice\"}.",
"oneOf": [
{
"enum": [
"all"
],
"type": "string"
},
{
"items": {
"type": "string"
},
"maxItems": 500,
"minItems": 1,
"type": "array"
},
{
"additionalProperties": {
"type": "string"
},
"type": "object"
}
]
},
"note": {
"description": "On drop/redo: the reason. Optional unless the org requires one.",
"maxLength": 1000,
"type": "string"
}
},
"required": [
"groupId",
"decision"
],
"type": "object"
},
"name": "decide_approval_group"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] action \"approve\" approves the section's pending pieces (or pieceIds) and their held posts GO LIVE at their slots — needs the explicit `review` scope (admin does not include it); QA-failed pieces are skipped unless overrideQa. \"fix\" sends the pieces back with a `note` (required): their posts are taken off the schedule and the section opens a new round; other sections are untouched. \"drop\" takes the posts off the schedule and drops the pieces. \"ready\" puts fixed pieces back up for review. fix/drop/ready need `review` or `publish`. Agency plan. Confirm with the user first.\n(Wraps POST /api/v2/campaigns/{id}/review/sections/{sectionId}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"action": {
"enum": [
"approve",
"fix",
"drop",
"ready"
],
"type": "string"
},
"id": {
"description": "Campaign id.",
"type": "string"
},
"note": {
"description": "Required for fix: what to change. Quoted in the next round.",
"maxLength": 1000,
"type": "string"
},
"overrideQa": {
"description": "Approve pieces that failed QA anyway.",
"type": "boolean"
},
"pieceIds": {
"description": "Only these pieces (\"approve selected\"). Default: every eligible piece in the section.",
"items": {
"type": "string"
},
"maxItems": 500,
"type": "array"
},
"sectionId": {
"type": "string"
}
},
"required": [
"id",
"sectionId",
"action"
],
"type": "object"
},
"name": "decide_campaign_review_section"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] decision \"approve\" sends a held post to the platform for its slot (GOES LIVE) — needs the explicit `review` scope (admin does not include it) and never works on a post the same connection created. decision \"hold\" takes a scheduled post off the platform schedule — needs `review` or `publish`. Agency plan. `id` is a reviewId or a convexPostId.\n(Wraps POST /api/v2/reviews/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"collabSide": {
"description": "FFE-501 collab posts only: which brand this decision is for (\"owner\" = the shared character's brand, \"grantee\" = the brand that made the post). Needed only when the key's owner approves for both brands; a brand-bound key always decides for its own brand. A collab post publishes only after BOTH sides approved; \"hold\" rejects it for good.",
"enum": [
"owner",
"grantee"
],
"type": "string"
},
"decision": {
"enum": [
"approve",
"hold"
],
"type": "string"
},
"id": {
"description": "A convexPostId or a review id.",
"type": "string"
},
"note": {
"maxLength": 1000,
"type": "string"
},
"pastSlot": {
"description": "Only for a held post whose slot passed: \"now\", or { at: ISO-8601 future time }.",
"oneOf": [
{
"enum": [
"now"
],
"type": "string"
},
{
"properties": {
"at": {
"format": "date-time",
"type": "string"
}
},
"required": [
"at"
],
"type": "object"
}
]
}
},
"required": [
"id",
"decision"
],
"type": "object"
},
"name": "decide_review"
},
{
"description": "[DESTRUCTIVE] DESTRUCTIVE: permanently deletes a content item from the library. Posts already live on a platform are not removed. Needs the `write` scope. Ask the user before calling.\n(Wraps DELETE /api/v2/content/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_content"
},
{
"description": "[DESTRUCTIVE] DESTRUCTIVE: deletes the experiment record (content keeps its labels). Ask the user before calling.\n(Wraps DELETE /api/v2/experiments/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_experiment"
},
{
"description": "[DESTRUCTIVE] DESTRUCTIVE: deletes a trigger (its logs stay). Ask the user before calling.\n(Wraps DELETE /api/v2/growth-triggers/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_growth_trigger"
},
{
"description": "[DESTRUCTIVE] DESTRUCTIVE: deletes the queue (its drafts stay in the library). With posts booked at the platform it is refused (409) unless `unschedule: true`, which first deletes those platform posts. Needs the `publish` scope (or admin) on the Agency plan or higher. Ask the user before calling.\n(Wraps DELETE /api/v2/queues/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "Account-wide keys: \"personal\" or an id from GET /api/v2/brands (omitted → the brand last active in the dashboard). A brand-bound key always uses its own brand.",
"type": "string"
},
"id": {
"type": "string"
},
"unschedule": {
"default": false,
"description": "true: delete every booked post at the platform first (the dashboard's \"Unschedule\"), then delete. Without it a booked post is a 409 — nothing is ever left booked at the platform without its queue item.",
"type": "boolean"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_queue"
},
{
"description": "[DESTRUCTIVE] DESTRUCTIVE: deletes a format (drafts started from it keep their text). Ask the user before calling.\n(Wraps DELETE /api/v2/text-templates/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_text_template"
},
{
"description": "[writes flayr data · `write` scope] Sets internal post labels and per-platform/account instanceLabels. Omitted sets stay unchanged; [] clears a set; labelMode set replaces both sets, add appends atomically. Use get_post to list both. Works after booking and publishing, and never contacts social networks. Labels are key:value (campaign, experiment, variant, repost, series, source-run, or free keys). Needs publish scope and the publishing plan.\n(Wraps PATCH /api/v2/posts/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"description": "The post: `convexPostId` from POST /api/v2/publish, or `posts[].id` from GET /api/v2/content/{id}.",
"type": "string"
},
"instanceLabels": {
"description": "Internal labels per platform/account instance. An array of {platform, accountId?, labels}; accountId is the Zernio connected account id. Omit accountId when unknown. instanceKey is derived as canonical platform + \":account:\" + URL-encoded account id, or \":pending\" when unknown (twitter canonicalizes to x). Replaces the instance set when editing; [] clears it. Never published.",
"items": {
"additionalProperties": false,
"properties": {
"accountId": {
"maxLength": 200,
"type": "string"
},
"labels": {
"items": {
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"platform": {
"type": "string"
}
},
"required": [
"platform",
"labels"
],
"type": "object"
},
"maxItems": 20,
"type": "array"
},
"labelMode": {
"description": "Default set. add atomically appends labels to the post and matching instances; other instances stay unchanged.",
"enum": [
"set",
"add"
],
"type": "string"
},
"labels": {
"description": "Sets this post's own labels ([] or null clears; labelMode add appends), e.g. [\"repost:evergreen\"]. flayr metadata: sent alone it never touches the booking (status \"labeled\") and works on posted posts too. With booking edits, metadata is saved after a successful edit. Normalized \"key:value\" labels (keys and values are lower-cased, spaces become \"-\"). Linked keys: campaign:<media campaign id>, experiment:<experiment id> (+ variant:<name> or the experiment's compareKey), type:<post type> (short, long-form, carousel, image, text, reel, story, poem, timelapse…). Free keys work too: cut:opus, lane:tech, parent:pantheon-part-1, ab:<pair id> (an A/B pair). Labels are copied onto every post made from the draft, and compared in GET /api/v2/analytics/experiments.",
"items": {
"pattern": "^[a-z0-9][a-z0-9_-]{0,31}:[a-z0-9][a-z0-9._\\-/+]{0,79}$",
"type": "string"
},
"maxItems": 30,
"type": [
"array",
"null"
]
}
},
"required": [
"id"
],
"type": "object"
},
"name": "edit_post_labels"
},
{
"description": "[read-only] Engagement overview for the last 7, 30 or 90 days. Pro plan or higher.\n(Wraps GET /api/v2/analytics.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"range": {
"default": "30d",
"enum": [
"7d",
"30d",
"90d"
],
"type": "string"
}
},
"type": "object"
},
"name": "get_analytics"
},
{
"description": "[read-only] A campaign's pieces grouped into review sections (episode, week, channel set, variant group or the creator's own). Each piece has preview URLs (video / images / audio), caption, labels, its bookings (channels, time, review state) and QA checks; each section has its status, round and every previous fix note. Share `reviewUrl` so a person can preview and decide in flayr.\n(Wraps GET /api/v2/campaigns/{id}/review.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"description": "Campaign id.",
"type": "string"
},
"pieces": {
"default": true,
"description": "false = sections and counts only.",
"type": "boolean"
},
"sectionId": {
"description": "Only this section.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_campaign_review"
},
{
"description": "[read-only] One content item plus its live posting status: `postState` and `posts[]` (up to 20, newest first) with each post's status (scheduled · publishing · published · partial · failed · review_held · held), scheduledAt, and per-platform status, live URL and error. The item's own `status` stays \"draft\" after publishing — read `posts` to know whether it is live. Use this to confirm a publish landed.\n(Wraps GET /api/v2/content/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_content"
},
{
"description": "[read-only] One experiment: hypothesis, variants, compareKey, window and status.\n(Wraps GET /api/v2/experiments/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_experiment"
},
{
"description": "[read-only] n per arm, pooled reach engagement rate, a 90% interval and a hedged verdict (likely_winner, no_clear_winner or not_enough_data). Missing numbers are \"not reported\" (null), never 0. Say \"likely\", never \"proven\".\n(Wraps GET /api/v2/experiments/{id}/readout.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"window": {
"default": "48h",
"enum": [
"48h",
"7d",
"30d",
"latest"
],
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_experiment_readout"
},
{
"description": "[read-only] The experiment's posts split by its compareKey, with the leading variant by its primary metric and n per arm. Small samples: read medians with their n.\n(Wraps GET /api/v2/experiments/{id}/results.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"include": {
"enum": [
"posts"
],
"type": "string"
},
"window": {
"default": "48h",
"enum": [
"48h",
"7d",
"latest"
],
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_experiment_results"
},
{
"description": "[read-only] A version test's rounds: each closed round's frozen snapshot, the open round's live numbers (null = not reported), the winner if one was recorded, and any repeat-winner proposal.\n(Wraps GET /api/v2/experiments/{id}/rounds.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_experiment_rounds"
},
{
"description": "[read-only] One library frame with its sizes and content areas.\n(Wraps GET /api/v2/frames/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_frame"
},
{
"description": "[read-only] Which frame each platform's images get automatically when a post is published or scheduled.\n(Wraps GET /api/v2/frames/settings.)",
"inputSchema": {
"additionalProperties": false,
"properties": {},
"type": "object"
},
"name": "get_frame_settings"
},
{
"description": "[read-only] One trigger with its keywords, texts, link, cap and counts.\n(Wraps GET /api/v2/growth-triggers/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_growth_trigger"
},
{
"description": "[read-only] Matched comments, unique commenters, replies, DMs and failures, in total and per day.\n(Wraps GET /api/v2/growth-triggers/{id}/stats.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_growth_trigger_stats"
},
{
"description": "[read-only] Status of an async job (combine_clips, render_motion_ad, or an API video generation): pending, processing, completed or failed. For a combine or a motion ad, `resultContentId` is the new draft once completed; a failed combine is refunded automatically. Poll every 10-30 s — renders take about a minute.\n(Wraps GET /api/v2/generate/status/{jobId}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"jobId": {
"type": "string"
}
},
"required": [
"jobId"
],
"type": "object"
},
"name": "get_job_status"
},
{
"description": "[read-only] A post (`id` = convexPostId from publish, or posts[].id from get_content): status, slot, platforms, its internal labels and per-platform/account instanceLabels, and while it is scheduled the first comment each platform will publish and whether it can be changed now.\n(Wraps GET /api/v2/posts/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"description": "The post: `convexPostId` from POST /api/v2/publish, or `posts[].id` from GET /api/v2/content/{id}.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_post"
},
{
"description": "[read-only] A queue and its items in order, with each item's state (queued · publishing · scheduled · posted · failed · skipped), booked or projected slot, and attribution status.\n(Wraps GET /api/v2/queues/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "Account-wide keys: \"personal\" or an id from GET /api/v2/brands (omitted → the brand last active in the dashboard). A brand-bound key always uses its own brand.",
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_queue"
},
{
"description": "[read-only] One format (text template) with its full structure and example.\n(Wraps GET /api/v2/text-templates/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_text_template"
},
{
"description": "[writes flayr data · `write` scope] Puts the campaign's not-yet-sectioned pieces into review sections (groupBy: week, channel_set, variant_group, episode or phase; default the brand's setting). Existing sections and decisions are kept.\n(Wraps POST /api/v2/campaigns/{id}/review.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"groupBy": {
"enum": [
"week",
"channel_set",
"variant_group",
"episode",
"phase"
],
"type": "string"
},
"id": {
"description": "Campaign id.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "group_campaign_review"
},
{
"description": "[writes flayr data · `write` scope] Writes a batch of draft hooks (on-screen card + spoken line) from the confirmed offers, claims-checked; blocked ones come back in `dropped`, unsaved. Charges Embers once per call (refunded when nothing usable comes back). Needs the Pro plan or higher.\n(Wraps POST /api/v2/hook-studio/hooks/generate.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"angles": {
"description": "e.g. pain, outcome, curiosity, proof.",
"items": {
"type": "string"
},
"type": "array"
},
"bodyIds": {
"description": "Bodies (from POST /api/v2/hook-studio/bodies/map) the hooks should lead into.",
"items": {
"type": "string"
},
"type": "array"
},
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"count": {
"maximum": 20,
"minimum": 1,
"type": "integer"
},
"formats": {
"description": "e.g. question, bold claim, number, POV.",
"items": {
"type": "string"
},
"type": "array"
},
"topics": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"name": "hooks_generate"
},
{
"description": "[read-only] The brand's hook bank (filter by status: draft, approved, retired), with each hook's claims result. Free. Needs the Pro plan or higher.\n(Wraps GET /api/v2/hook-studio/hooks.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"status": {
"enum": [
"draft",
"approved",
"retired"
],
"type": "string"
}
},
"type": "object"
},
"name": "hooks_list"
},
{
"description": "[writes flayr data · `write` scope] Edits a hook or sets its status. New copy is claims-checked again; a hook whose claims block cannot be approved. Free. Needs the Pro plan or higher.\n(Wraps PATCH /api/v2/hook-studio/hooks.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"angle": {
"type": "string"
},
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"card": {
"type": "string"
},
"fit": {
"items": {
"type": "string"
},
"type": "array"
},
"format": {
"type": "string"
},
"id": {
"type": "string"
},
"line": {
"type": "string"
},
"status": {
"enum": [
"draft",
"approved",
"retired"
],
"type": "string"
},
"style": {
"enum": [
"question",
"reveal",
"joke",
"how-to",
"hot-take",
"story"
],
"type": "string"
},
"topics": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "hooks_update"
},
{
"description": "[writes flayr data · `write` scope] Adds and/or removes labels on up to 100 content items in one call: each item becomes (labels − remove) + add. Published items are fine (labels are metadata). Each item succeeds or fails on its own; read `results`. Removing `source:ai-generated` does not turn a post's AI label off: with no explicit toggle it follows how the media was made, and the result carries a `warning`. For real footage also call update_content with `aiGenerated: false` on each item.\n(Wraps POST /api/v2/content/labels.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"add": {
"description": "Labels to add. Normalized \"key:value\" labels (keys and values are lower-cased, spaces become \"-\"). Linked keys: campaign:<media campaign id>, experiment:<experiment id> (+ variant:<name> or the experiment's compareKey), type:<post type> (short, long-form, carousel, image, text, reel, story, poem, timelapse…). Free keys work too: cut:opus, lane:tech, parent:pantheon-part-1, ab:<pair id> (an A/B pair). Labels are copied onto every post made from the draft, and compared in GET /api/v2/analytics/experiments.",
"items": {
"pattern": "^[a-z0-9][a-z0-9_-]{0,31}:[a-z0-9][a-z0-9._\\-/+]{0,79}$",
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"ids": {
"description": "Content ids (GET /api/v2/content).",
"items": {
"type": "string"
},
"maxItems": 100,
"minItems": 1,
"type": "array"
},
"remove": {
"description": "Labels to remove. Not also in `add`.",
"items": {
"pattern": "^[a-z0-9][a-z0-9_-]{0,31}:[a-z0-9][a-z0-9._\\-/+]{0,79}$",
"type": "string"
},
"maxItems": 30,
"type": "array"
}
},
"required": [
"ids"
],
"type": "object"
},
"name": "labels_bulk_edit"
},
{
"description": "[read-only] The same held content as list_approvals, grouped by piece: the variants of one draft (or sharing a `group:<id>` label), with the axes they vary on (theme, hook, audio …), a tally (approved / redo / dropped / open) and each variant's state and slot time. `status` open (default) / redo / approved / dropped / all. Needs the explicit `approve` scope. Agency plan.\n(Wraps GET /api/v2/approval-groups.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brand": {
"description": "\"personal\" or a brand id (`brandId` is accepted too).",
"type": "string"
},
"status": {
"default": "open",
"enum": [
"open",
"redo",
"approved",
"dropped",
"all"
],
"type": "string"
}
},
"type": "object"
},
"name": "list_approval_groups"
},
{
"description": "[read-only] Content held for approval that waits on the account owner's decision, newest first: posts filed with publish `requiresApproval: true`, drafts submitted for review, and workflow runs parked at an approval gate. `kind` is \"manual\" or \"workflow\". With status \"rejected\" it lists rejected content instead, with the reviewer's reason (`note`, `reasonChips`), who rejected it and when. Needs the explicit `approve` scope (admin does not include it). Agency plan.\n(Wraps GET /api/v2/approvals.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"type": "string"
},
"status": {
"default": "pending",
"enum": [
"pending",
"rejected"
],
"type": "string"
}
},
"type": "object"
},
"name": "list_approvals"
},
{
"description": "[read-only] The brand's \"posts like this must carry a credit\" rules and its mode (flag / push_down). In push_down mode publish refuses or holds a post missing a required credit — add `credits` with update_content first.\n(Wraps GET /api/v2/attribution-rules.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "Account-wide keys: \"personal\" or an id from GET /api/v2/brands (omitted → the brand last active in the dashboard). A brand-bound key always uses its own brand.",
"type": "string"
}
},
"type": "object"
},
"name": "list_attribution_rules"
},
{
"description": "[read-only] Lists the brands this connection can file content into and publish through. Use an `id` as `brandId` on create_content, publish, queues and analytics tools; \"personal\" means the account owner's personal accounts. A connection bound to one brand sees only that brand (and personal is null). Each brand has its `website` and a `setup` checklist (hasBrandKey, hasWebsite, hasCtaGoal, adsConnected): set a missing website with brand_profile_update; a brand API key is created by the owner in Settings, never by an agent.\n(Wraps GET /api/v2/brands.)",
"inputSchema": {
"additionalProperties": false,
"properties": {},
"type": "object"
},
"name": "list_brands"
},
{
"description": "[read-only] The social accounts a brand can post to — the same set publish would use for that `brandId`: platform, handle, display name, health (healthy, needs_reauth, disconnected) and whether flayr can post there right now (`postable`; Reddit is paused because Reddit closed its API to third-party apps). Call this before publish to pick platforms, and tell the user to reconnect any account that is not healthy at flayr.social/connect.\n(Wraps GET /api/v2/accounts.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "An id from GET /api/v2/brands, or \"personal\". Omitted → the key's bound brand, else the owner's personal accounts.",
"type": "string"
}
},
"type": "object"
},
"name": "list_connected_accounts"
},
{
"description": "[read-only] Lists content items (drafts), newest first. Each carries `postState` (\"posted\" / \"scheduled\" / null) and `posts[]` (up to 5): where it went out, per platform, with status, live URL and failure reason.\n(Wraps GET /api/v2/content.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. Omitted: the key's brand (brand-bound key) or the owner's active brand.",
"type": "string"
},
"includeArchived": {
"default": false,
"type": "boolean"
},
"limit": {
"default": 20,
"maximum": 200,
"minimum": 1,
"type": "integer"
}
},
"type": "object"
},
"name": "list_content"
},
{
"description": "[read-only] A/B experiments. Content joins one by carrying the label `experiment:<id>` plus `<compareKey>:<variant>`.\n(Wraps GET /api/v2/experiments.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"allBrands": {
"description": "true: every brand of the organizations you own or administer, plus your personal brands. Refused (403) for a plain member and for a brand-bound key.",
"type": "boolean"
},
"brandId": {
"description": "A brand id, or \"all\" (default: every profile you can see). A brand-bound key is always its own brand.",
"type": "string"
}
},
"type": "object"
},
"name": "list_experiments"
},
{
"description": "[read-only] The brand's frames (borders, overlays, lower thirds, corner tags, quote-card frames, ribbons) plus the built-in flayr frames in the brand colors. Each has a `ref` to pass to apply_frame.\n(Wraps GET /api/v2/frames.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"aspect": {
"enum": [
"1:1",
"4:5",
"9:16",
"16:9"
],
"type": "string"
},
"includeArchived": {
"default": false,
"type": "boolean"
},
"kind": {
"enum": [
"border",
"overlay",
"lower_third",
"corner_bug",
"quote_card",
"ribbon"
],
"type": "string"
}
},
"type": "object"
},
"name": "list_frames"
},
{
"description": "[read-only] The brand's keyword triggers: a comment matching a keyword gets a public reply and/or a DM. Shows whether each is active and its counts.\n(Wraps GET /api/v2/growth-triggers.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
}
},
"type": "object"
},
"name": "list_growth_triggers"
},
{
"description": "[read-only] Boards of the Pinterest account(s) a brand publishes to. Use a board id as `pinterest.boardId` on publish. A board is required; the one you pick is remembered as the brand's default for next time, and a publish that uses the default says so (defaultBoardApplied).\n(Wraps GET /api/v2/pinterest/boards.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "An id from GET /api/v2/brands, or \"personal\". Omitted → the key's bound brand, else the owner's personal accounts.",
"type": "string"
}
},
"type": "object"
},
"name": "list_pinterest_boards"
},
{
"description": "[read-only] A post's comments-after-publish and reshares, with status (waiting, needs_approval, sent, failed) and the resolved text.\n(Wraps GET /api/v2/posts/{id}/comments.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "list_post_comments"
},
{
"description": "[read-only] Pending, sent and failed comments-after-publish and reshares of the brand (filter by `status`, e.g. needs_approval or failed), with what each waits on.\n(Wraps GET /api/v2/automations/actions.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "Account-wide keys: \"personal\" or an id from GET /api/v2/brands (omitted → the brand last active in the dashboard). A brand-bound key always uses its own brand.",
"type": "string"
},
"status": {
"enum": [
"waiting",
"needs_approval",
"sending",
"sent",
"failed",
"cancelled",
"rejected"
],
"type": "string"
}
},
"type": "object"
},
"name": "list_publish_actions"
},
{
"description": "[read-only] Posting (drip) queues of one brand. A queue posts one item every cadenceHours at timeOfDay in its timezone, keeping `lookahead` items booked at the platform.\n(Wraps GET /api/v2/queues.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "Account-wide keys: \"personal\" or an id from GET /api/v2/brands (omitted → the brand last active in the dashboard). A brand-bound key always uses its own brand.",
"type": "string"
}
},
"type": "object"
},
"name": "list_queues"
},
{
"description": "[read-only] Scheduled posts waiting for (or given) a human sign-off, oldest slot first. `canApprove` tells whether THIS connection may approve each one.\n(Wraps GET /api/v2/reviews.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"type": "string"
},
"status": {
"default": "pending",
"enum": [
"pending",
"approved",
"held",
"missed",
"all"
],
"type": "string"
},
"window": {
"default": "all",
"enum": [
"in",
"upcoming",
"all"
],
"type": "string"
}
},
"type": "object"
},
"name": "list_reviews"
},
{
"description": "[read-only] The brand's share rules: \"when <account> publishes, reshare to <accounts> (or comment on it) after N minutes with text T\", each with its approval mode.\n(Wraps GET /api/v2/automations/reshare-rules.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "Account-wide keys: \"personal\" or an id from GET /api/v2/brands (omitted → the brand last active in the dashboard). A brand-bound key always uses its own brand.",
"type": "string"
}
},
"type": "object"
},
"name": "list_share_rules"
},
{
"description": "[read-only] Reusable text shapes per brand: sections, length targets, hook patterns, CTA slots and guardrails. Use one to shape a draft's text before create_content.\n(Wraps GET /api/v2/text-templates.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id (GET /api/v2/brands), \"personal\", or \"all\" (default: everything the key owner can see). A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"includeArchived": {
"default": false,
"type": "boolean"
}
},
"type": "object"
},
"name": "list_text_templates"
},
{
"description": "[read-only] Playlists of the YouTube channel(s) a brand publishes to. Use a playlist id as `youtube.playlistId` on create_content or publish.\n(Wraps GET /api/v2/youtube/playlists.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "An id from GET /api/v2/brands, or \"personal\". Omitted → the key's bound brand, else the owner's personal accounts.",
"type": "string"
}
},
"type": "object"
},
"name": "list_youtube_playlists"
},
{
"description": "[read-only] Reads the cached video/audio QA report, timestamped findings and approval/booking gate. Missing or failed QA blocks. API keys cannot override.\n(Wraps GET /api/v2/content/{id}/qa.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "media_qa_get"
},
{
"description": "[writes flayr data · `write` scope] Queues a free media QA run using the existing per-minute cap. Poll media_qa_get. Does not approve, book or override.\n(Wraps POST /api/v2/content/{id}/qa/rerun.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "media_qa_rerun"
},
{
"description": "[writes flayr data · `write` scope] Moves the planned time of one or many items waiting for approval. Moving never approves and books nothing. Needs the explicit `approve` scope (admin does not include it). Agency plan.\n(Wraps POST /api/v2/approvals/move.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"ids": {
"items": {
"type": "string"
},
"maxItems": 100,
"minItems": 1,
"type": "array"
},
"to": {
"description": "Exactly one of: { at: ISO-8601 }; { shiftDays, shiftMinutes? } from each item's own time; { date: \"YYYY-MM-DD\", time?: \"HH:mm\", timeZone } that day, at `time` or each item's own time of day.",
"properties": {
"at": {
"format": "date-time",
"type": "string"
},
"date": {
"type": "string"
},
"shiftDays": {
"type": "integer"
},
"shiftMinutes": {
"type": "integer"
},
"time": {
"type": "string"
},
"timeZone": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"ids",
"to"
],
"type": "object"
},
"name": "move_approvals"
},
{
"description": "[writes flayr data · `write` scope] Moves one or many posts held for review (\"move to Tuesday 10:00\", \"+1 day\"). Moving never approves and nothing goes live. A post already booked at the platform answers vendor_booked; use update_scheduled_post. Needs `review` or `publish`. Agency plan.\n(Wraps POST /api/v2/reviews/move.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"ids": {
"description": "Review ids or convexPostIds.",
"items": {
"type": "string"
},
"maxItems": 100,
"minItems": 1,
"type": "array"
},
"to": {
"description": "Exactly one of: { at: ISO-8601 }; { shiftDays, shiftMinutes? } from each post's own slot; { date: \"YYYY-MM-DD\", time?: \"HH:mm\", timeZone } that day, at `time` or each post's own time of day.",
"properties": {
"at": {
"format": "date-time",
"type": "string"
},
"date": {
"type": "string"
},
"shiftDays": {
"type": "integer"
},
"shiftMinutes": {
"type": "integer"
},
"time": {
"type": "string"
},
"timeZone": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"ids",
"to"
],
"type": "object"
},
"name": "move_reviews"
},
{
"description": "[writes flayr data · `write` scope] Turns what the brand sells (pasted text, or answers to the offer interview) into draft offers in its offer bank, with prices from the plan catalog, Shopify or the text. Charges Embers once per call (refunded when nothing usable comes back). Needs the Pro plan or higher.\n(Wraps POST /api/v2/hook-studio/offers.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"answers": {
"items": {
"properties": {
"answer": {
"type": "string"
},
"question": {
"type": "string"
}
},
"required": [
"question",
"answer"
],
"type": "object"
},
"type": "array"
},
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"source": {
"enum": [
"interview",
"manual"
],
"type": "string"
},
"text": {
"description": "What the brand sells, in any words.",
"maxLength": 5000,
"type": "string"
}
},
"type": "object"
},
"name": "offers_capture"
},
{
"description": "[read-only] The brand's offer bank (filter by status: draft, confirmed, retired). Free. Needs the Pro plan or higher.\n(Wraps GET /api/v2/hook-studio/offers.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"status": {
"enum": [
"draft",
"confirmed",
"retired"
],
"type": "string"
}
},
"type": "object"
},
"name": "offers_list"
},
{
"description": "[writes flayr data · `write` scope] Edits an offer's fields or status; \"confirmed\" makes it usable for hooks and CTAs. Confirm changes with the person first. Free. Needs the Pro plan or higher.\n(Wraps PATCH /api/v2/hook-studio/offers.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"audience": {
"type": "string"
},
"brandId": {
"description": "A brand id from GET /api/v2/brands. Required for an account-wide key; a brand-bound key may omit it.",
"type": "string"
},
"currency": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"outcomes": {
"items": {
"type": "string"
},
"type": "array"
},
"pains": {
"items": {
"type": "string"
},
"type": "array"
},
"priceAmount": {
"type": "number"
},
"priceRef": {
"type": "string"
},
"priceSource": {
"enum": [
"plan_catalog",
"shopify",
"manual"
],
"type": "string"
},
"proof": {
"items": {
"type": "string"
},
"type": "array"
},
"proofUrl": {
"type": "string"
},
"status": {
"enum": [
"draft",
"confirmed",
"retired"
],
"type": "string"
},
"summary": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "offers_update"
},
{
"description": "[read-only] Teardown Studio: send a 3D model (`model`, base64 STL, 3MF or GLB, up to 3 MB inline) and get its parts list, a default teardown/1 plan (hold, explode, label the biggest parts, slow-motion, reassemble, reveal, brand end card; 16:9 and 9:16) built from part count and bounding boxes, and the render estimate (frames, worker time; free). Rename parts with `names` ({ p0: \"Lid\" }). Or send an edited `plan` instead of a model to check it against the limits and re-estimate. Nothing is stored, rendered or charged. A model that is not a physical object (status render or prototype) is labelled \"3D render\" in the film.\n(Wraps POST /api/v2/video-studio/templates/teardown/plan.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"accent": {
"description": "#RRGGBB end-card accent.",
"type": "string"
},
"brandName": {
"maxLength": 100,
"type": "string"
},
"engraving": {
"description": "Show the NAME engraving placeholder on the reveal.",
"type": "boolean"
},
"fileName": {
"description": "Helps detect the format, e.g. caddy.stl.",
"maxLength": 200,
"type": "string"
},
"model": {
"description": "Base64 of the model file (STL, 3MF or GLB). Send this or `plan`.",
"type": "string"
},
"names": {
"description": "Part id -> name, e.g. { \"p0\": \"Lid\" }.",
"type": "object"
},
"plan": {
"description": "An edited teardown/1 plan (from an earlier call). Send this or `model`.",
"type": "object"
},
"status": {
"description": "Is this a real, made object? Anything but physical gets a \"3D render\" label.",
"enum": [
"render",
"prototype",
"physical"
],
"type": "string"
},
"title": {
"description": "Film title and end-card headline.",
"maxLength": 200,
"type": "string"
}
},
"type": "object"
},
"name": "plan_teardown"
},
{
"description": "[read-only] How every flayr voice (chat voice, voiceovers, character VO) says a brand's words: `pronunciations` (term → sayAs respelling, with `verified` when a Verify passed), `suggested` (learned suggestions, inert until accepted) and `builtIn` entries. Captions and transcripts keep the written word. `brandId` from list_brands, or `personal`.\n(Wraps GET /api/v2/pronunciations.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "Brand id, or `personal`. Default: a bound key's brand, else your active brand.",
"type": "string"
}
},
"type": "object"
},
"name": "pronunciations_list"
},
{
"description": "[writes flayr data · `write` scope] Sets how voices say `term` for a brand: `sayAs` is a plain respelling (e.g. \"Dahss-tuhl\"). Updates the entry with that term (case-insensitive; accepts a waiting suggestion) or creates one; fields left out keep their values. `acceptedHearings` are transcriptions Verify also counts as a pass. Applies to every voice from the next render, with no per-call setup.\n(Wraps POST /api/v2/pronunciations/upsert.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"acceptedHearings": {
"description": "Transcriptions Verify also counts as a pass.",
"items": {
"type": "string"
},
"maxItems": 20,
"type": "array"
},
"alsoRecord": {
"description": "Also tell a human voice actor (default true).",
"type": "boolean"
},
"brandId": {
"description": "Brand id, or `personal`.",
"type": "string"
},
"note": {
"type": "string"
},
"sayAs": {
"maxLength": 120,
"type": "string"
},
"spellExact": {
"description": "Read it letter by letter.",
"type": "boolean"
},
"term": {
"maxLength": 80,
"type": "string"
}
},
"required": [
"term",
"sayAs"
],
"type": "object"
},
"name": "pronunciations_upsert"
},
{
"description": "[writes flayr data · `write` scope] Speaks a carrier sentence with the entry in a voice (default Gemini Puck), transcribes it, and passes when the transcript has the term or an accepted hearing. CHARGES EMBERS per check: only call it when the user asks, after telling them the price (GET /api/v2/pronunciations/{id}/verify quotes it; nothing is charged for a quote). Never refunded, even when the check fails or cannot run. `id` from pronunciations_list.\n(Wraps POST /api/v2/pronunciations/{id}/verify.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "Brand id, or `personal`.",
"type": "string"
},
"id": {
"description": "Pronunciation id (GET /api/v2/pronunciations).",
"type": "string"
},
"model": {
"type": "string"
},
"voice": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "pronunciations_verify"
},
{
"description": "[writes flayr data · `write` scope] Proposes slots to post the winning version again (default 3 over 2 weeks). Only a proposal: nothing is booked or posted until the user confirms it in flayr, and then each post still goes through approval.\n(Wraps POST /api/v2/experiments/{id}/repeat-winner.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"platforms": {
"items": {
"type": "string"
},
"maxItems": 10,
"minItems": 1,
"type": "array"
},
"startAt": {
"description": "First slot; default now.",
"oneOf": [
{
"format": "date-time",
"type": "string"
},
{
"type": "number"
}
]
}
},
"required": [
"id"
],
"type": "object"
},
"name": "propose_repeat_winner"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] PUBLISHES TO REAL SOCIAL ACCOUNTS. Posts now, or at `scheduledAt` (ISO-8601, future). Prefer `contentId` (a draft from create_content: its text and media become the post) over inline `content`. The post goes out through the target brand's own connected accounts. Always pass an `idempotencyKey` so a retry never double-posts. `platformText` ({ twitter: \"…\", linkedin: \"…\" }) gives each platform its own caption, as update_scheduled_post takes it (saved on the draft with contentId); unknown arguments are refused (422 unknown_fields). The same safety gates as the dashboard apply: platform readiness/media rules are checked up front (422); if the brand requires scheduled-post review nothing is sent and you get { status: \"pending-review\", reviewId }; a missing required creator credit in a push-down brand is refused (422 attribution_missing) or held; the \"Posted with flayr.social\" caption watermark and video watermark follow the plan (409 watermark_pending means the watermarked video is still rendering — retry after retryAfterSeconds); 409 duplicate_content means the same content already went to that account in the last 24 hours (existingPostId names it; nothing new was posted). `facebook: { contentType: \"reel\" }` posts a Facebook Reel (exactly one video), `\"story\"` a Story, `\"feed\"` a regular post; `instagram.contentType` works the same; omitted → the draft's saved post type. `instagram.trialReel: { enabled: true, graduation: \"manual\" | \"auto\" }` posts a Reel to non-followers first (Instagram Trial Reel; omitted graduation → admin default; ignored with a warning when not a Reel). `pinterest: { boardId, title, link }` picks the board (ids from list_pinterest_boards; required: omitted → the brand's last-picked board for that account, flagged as defaultBoardApplied, else 422 pinterest_board_required), pin title and destination link. `requiresApproval: true` files it for human approval instead of posting. A draft that is already scheduled on one of the platforms is refused (409 already_booked): change the booked post instead, or pass `allowDuplicateBooking: true` only when the user asked to post it again. Needs the `publish` scope (or admin) and the Pro plan or higher; each publish counts against the plan's direct posts for the month. Confirm with the user before posting now. Then call get_content to see the live URLs.\n(Wraps POST /api/v2/publish.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"aiGenerated": {
"description": "Label the post as AI-generated / synthetic media: YouTube containsSyntheticMedia, Instagram isAiGenerated, TikTok videoMadeWithAi, X madeWithAi, and the disclosure line (default \"#AIgenerated\") on platforms with no label field. Omitted → the content item's own setting, else on when its media was made with flayr AI (a character, a cloned voice, generated video or images). false on flayr-generated media posts without a label and adds a warning. A character post always carries the label.",
"type": "boolean"
},
"allowDuplicateBooking": {
"default": false,
"description": "A contentId that is already scheduled (a live booking still ahead) on one of these platforms is refused with 409 { code: \"already_booked\", bookedAt, bookedPlatforms }. Pass true only to book the same draft again on purpose.",
"type": "boolean"
},
"autoFirstComment": {
"default": true,
"description": "When the target brand has \"Auto first comment\" on (dashboard: Brand → First comment), LinkedIn, Instagram, Facebook, Threads and YouTube targets that have no first comment of their own get the brand's comment, with a tracked link per platform. false posts this one without it. No effect when the brand has it off.",
"type": "boolean"
},
"brandId": {
"description": "An id from GET /api/v2/brands, or \"personal\". With contentId it must match the item's brand.",
"type": "string"
},
"content": {
"description": "Post text. Required unless contentId is given.",
"maxLength": 10000,
"minLength": 1,
"type": "string"
},
"contentId": {
"description": "An existing content item id (GET /api/v2/content/{id} access rules). Cannot be combined with imageUrl / videoUrl.",
"type": "string"
},
"facebook": {
"additionalProperties": false,
"description": "Facebook post type. \"reel\" posts a Reel (exactly one video, no images; anything else is a 422 with code content_type_invalid), \"story\" a Page Story, \"feed\" a regular post. Omitted → the content item's stored platformSpecificData.facebook.contentType (what the composer saved), else a regular post.",
"properties": {
"contentType": {
"enum": [
"reel",
"story",
"feed"
],
"type": "string"
}
},
"type": "object"
},
"firstComment": {
"additionalProperties": false,
"description": "Explicit first comments per platform, posted right after the post goes out. Always wins over the brand's auto first comment. Threads: posted as a reply (it counts toward Threads' 250 posts/24h). Facebook and Instagram: not on Stories. YouTube: use youtube.firstComment. Not supported on TikTok, X, Bluesky or Pinterest.",
"properties": {
"facebook": {
"maxLength": 8000,
"type": "string"
},
"instagram": {
"maxLength": 2200,
"type": "string"
},
"linkedin": {
"maxLength": 1250,
"type": "string"
},
"threads": {
"maxLength": 500,
"type": "string"
}
},
"type": "object"
},
"followUpComments": {
"description": "Comments after publish, sent once each (Idempotency-Key) after the post is live on that platform and its tokens resolve: {{postUrl}}, {{postUrl:<platform>}}, {{parentPostUrl:<post or draft id>}}, {{youtubeUrl:<post or draft id>}}. TikTok: TikTok for Business accounts only (unverified); not on Pinterest.",
"items": {
"properties": {
"delayMinutes": {
"minimum": 0,
"type": "integer"
},
"platform": {
"type": "string"
},
"sendAt": {
"format": "date-time",
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"platform",
"text"
],
"type": "object"
},
"maxItems": 20,
"type": "array"
},
"idempotencyKey": {
"description": "Unique per API key. A retry with the same key never posts twice.",
"maxLength": 200,
"type": "string"
},
"imageUrl": {
"type": "string"
},
"instagram": {
"additionalProperties": false,
"description": "Instagram post type, same rules: \"reel\" (exactly one video — a single video is posted as a Reel), \"story\", or \"feed\". Omitted → the content item's stored platformSpecificData.instagram.contentType. `trialReel` posts the Reel as an Instagram Trial Reel (shown to non-followers first): `graduation` \"manual\" (graduate it from the Instagram app) or \"auto\" (Instagram graduates it when it performs well); omitted → the admin default. Reels only: on a Story, images or a carousel it is ignored with a warning. A trial post is labelled `trial-reel:<graduation>`.",
"properties": {
"contentType": {
"enum": [
"reel",
"story",
"feed"
],
"type": "string"
},
"trialReel": {
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"graduation": {
"enum": [
"manual",
"auto"
],
"type": "string"
}
},
"required": [
"enabled"
],
"type": "object"
}
},
"type": "object"
},
"instanceLabels": {
"description": "Internal labels per platform/account instance. An array of {platform, accountId?, labels}; accountId is the Zernio connected account id. Omit accountId when unknown. instanceKey is derived as canonical platform + \":account:\" + URL-encoded account id, or \":pending\" when unknown (twitter canonicalizes to x). Replaces the instance set when editing; [] clears it. Never published.",
"items": {
"additionalProperties": false,
"properties": {
"accountId": {
"maxLength": 200,
"type": "string"
},
"labels": {
"items": {
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"platform": {
"type": "string"
}
},
"required": [
"platform",
"labels"
],
"type": "object"
},
"maxItems": 20,
"type": "array"
},
"labels": {
"description": "Labels for this post on top of the draft's own (the draft wins for type/cut/variant). Normalized \"key:value\" labels (keys and values are lower-cased, spaces become \"-\"). Linked keys: campaign:<media campaign id>, experiment:<experiment id> (+ variant:<name> or the experiment's compareKey), type:<post type> (short, long-form, carousel, image, text, reel, story, poem, timelapse…). Free keys work too: cut:opus, lane:tech, parent:pantheon-part-1, ab:<pair id> (an A/B pair). Labels are copied onto every post made from the draft, and compared in GET /api/v2/analytics/experiments.",
"items": {
"pattern": "^[a-z0-9][a-z0-9_-]{0,31}:[a-z0-9][a-z0-9._\\-/+]{0,79}$",
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"mediaCampaignId": {
"description": "Link to a media campaign; with a brand target it must belong to that brand.",
"type": "string"
},
"memo": {
"description": "\"When it posts\" memo: the people-steps after the post goes live (reply to a comment, share to a personal account, pin a link, add a first comment flayr can't). Saved on the draft (else the post). When the post goes live, the brand's recipients (Brand → When a post goes live; default owners + admins) get ONE notice — in-app, email and push per their own settings — linking /publish/live/{convexPostId}: live links, the shared checklist, Copy buttons and a share link.",
"properties": {
"notes": {
"maxLength": 4000,
"type": "string"
},
"tasks": {
"items": {
"oneOf": [
{
"description": "The to-do, e.g. \"Share to personal LinkedIn\".",
"maxLength": 500,
"type": "string"
},
{
"properties": {
"copyText": {
"description": "Text to paste (a reply, a first comment). The live page shows a Copy button.",
"maxLength": 5000,
"type": "string"
},
"id": {
"description": "PUT only: keep an existing to-do (and its tick) by id.",
"type": "string"
},
"platform": {
"description": "The platform the step is for, e.g. \"youtube\".",
"type": "string"
},
"text": {
"maxLength": 500,
"type": "string"
},
"url": {
"description": "Where to do it (http/https).",
"format": "uri",
"type": "string"
}
},
"required": [
"text"
],
"type": "object"
}
]
},
"maxItems": 30,
"type": "array"
}
},
"type": "object"
},
"onPublishTasks": {
"description": "Shorthand for memo.tasks (appended after them). Response: `memoId`; a memo that could not be saved is a `warnings` entry, never a failed publish.",
"items": {
"oneOf": [
{
"description": "The to-do, e.g. \"Share to personal LinkedIn\".",
"maxLength": 500,
"type": "string"
},
{
"properties": {
"copyText": {
"description": "Text to paste (a reply, a first comment). The live page shows a Copy button.",
"maxLength": 5000,
"type": "string"
},
"id": {
"description": "PUT only: keep an existing to-do (and its tick) by id.",
"type": "string"
},
"platform": {
"description": "The platform the step is for, e.g. \"youtube\".",
"type": "string"
},
"text": {
"maxLength": 500,
"type": "string"
},
"url": {
"description": "Where to do it (http/https).",
"format": "uri",
"type": "string"
}
},
"required": [
"text"
],
"type": "object"
}
]
},
"maxItems": 30,
"type": "array"
},
"pinterest": {
"additionalProperties": false,
"description": "FFE-428: Pinterest pin options for every Pinterest target. Overrides the content item's stored platformSpecificData.pinterest field by field. A board is required. boardId omitted → the brand's last-picked board for that Pinterest account, flagged in the response (defaultBoardApplied, defaultBoardName); with no default the publish is refused (422 pinterest_board_required). The board picked is remembered as the default. title omitted → the first line of the caption.",
"properties": {
"boardId": {
"description": "From GET /api/v2/pinterest/boards for the same brand.",
"maxLength": 200,
"type": "string"
},
"link": {
"description": "Destination link (http or https).",
"format": "uri",
"maxLength": 2048,
"type": "string"
},
"title": {
"maxLength": 100,
"minLength": 1,
"type": "string"
}
},
"type": "object"
},
"platformText": {
"additionalProperties": {
"maxLength": 10000,
"type": [
"string",
"null"
]
},
"description": "Per-platform captions for this post, the same shape, validation and limits as PATCH /api/v2/posts/{id}: { \"twitter\": \"…\" } gives X its own caption, null / \"\" clears one, null clears them all; an over-limit caption is a 422. With contentId they are merged into the item's stored captions and saved on it first (so a later edit, re-book or review uses the same ones); with `content` they apply on top of that text. Platforms without one get the text, auto-fit. Per-platform captions: platform id → the caption that platform gets instead of `text`, e.g. { \"twitter\": \"short version\" }. Each must fit its platform: X 280 (a link counts 23, an emoji 2), Bluesky 300, Threads 500, Instagram 2200, LinkedIn 3000. Platforms without one get `text` when it fits, else an automatic short version: the first line, the credits in short form, as many hashtags as fit and the short flayr link (hashtags go first, then the credit link becomes \"Model by <author>\"; words are never cut). PATCH merges: a null (or \"\") value clears that platform, platformText: null clears all. At publish every platform is validated on its own caption: one failing platform no longer fails the others — the post publishes to the rest and reports the failures in platformErrors.",
"type": [
"object",
"null"
]
},
"platforms": {
"items": {
"enum": [
"instagram",
"tiktok",
"youtube",
"youtube_shorts",
"linkedin",
"pinterest",
"twitter",
"facebook",
"threads",
"bluesky",
"snapchat",
"googlebusiness",
"google_business",
"reddit",
"telegram",
"discord"
],
"type": "string"
},
"maxItems": 10,
"minItems": 1,
"type": "array"
},
"poll": {
"additionalProperties": false,
"description": "A native poll. Only on twitter (X), linkedin and discord, and with no media: any other platform or media is a 422 (code poll_invalid), never a plain post. X: 2-4 choices of at most 25 characters, 5 minutes to 7 days, and the post text is the question. LinkedIn: question (at most 140) required, 2-4 choices of at most 30 characters, 1, 3, 7 or 14 days. Discord: question (at most 300) required, 2-10 choices, whole hours up to 768. Cannot be combined with requiresApproval, or with a brand whose scheduled-post review is required (409 poll_not_holdable). Vote counts are not available through the API.",
"properties": {
"durationMinutes": {
"default": 1440,
"minimum": 5,
"type": "integer"
},
"options": {
"items": {
"minLength": 1,
"type": "string"
},
"maxItems": 10,
"minItems": 2,
"type": "array"
},
"question": {
"maxLength": 300,
"type": "string"
}
},
"required": [
"options"
],
"type": "object"
},
"referralFooter": {
"description": "Caption watermark: appends \"Posted with flayr.social — try it: flayr.social/r/<code>/<ch>\" to each platform's text (<code> is your referral code — your custom one if you claimed it; <ch> names the platform: yt, x, li, ig, tt, fb, th, bs). (YouTube: description only, never the title; X: the shorter \"via flayr.social/r/<code>/x\" or nothing when it would not fit — your text is never cut). On by default. false removes it only on plans that can remove the Flayr watermark (Pro and up); omitted follows your watermark setting. Beats the content item's stored value.",
"type": "boolean"
},
"requiresApproval": {
"default": false,
"type": "boolean"
},
"reshare": {
"additionalProperties": false,
"description": "Share/repost another post: a flayr post id (`postId`) or a pasted link (`url`). Per platform: linkedin a native reshare with your text on top (not with media or a poll); twitter a quote of your own X post, otherwise a retweet of it fired once this post publishes (no text); every other platform a link share — a new post carrying the original's link, because it has no repost API. X alone that can only retweet is a 422. Cannot be combined with requiresApproval.",
"properties": {
"postId": {
"type": "string"
},
"url": {
"type": "string"
}
},
"type": "object"
},
"scheduledAt": {
"description": "Must be in the future. Omitted → publish now.",
"format": "date-time",
"type": "string"
},
"strictMedia": {
"default": false,
"description": "Refuse (422 media_fit) when the video does not fit a booked platform (length, shape, smallest frame, loudness) instead of posting with `warnings`.",
"type": "boolean"
},
"videoUrl": {
"type": "string"
},
"youtube": {
"description": "Overrides the content item's stored YouTube options field by field. YouTube upload options, applied to YouTube targets only. madeForKids is always sent to YouTube (default false). A missing title is derived from the first sentence of the caption, cut to 100 characters at a word boundary. SCHEDULED UPLOADS: the video is uploaded ahead of scheduledAt. \"public\" is uploaded as private and released by YouTube at the scheduled time; \"unlisted\" is watchable by anyone with the link from upload time; \"private\" stays private.",
"properties": {
"categoryId": {
"description": "YouTube category id, e.g. \"28\" (Science & Technology). YouTube default: 22.",
"pattern": "^\\d{1,3}$",
"type": "string"
},
"containsSyntheticMedia": {
"description": "AI-generated content disclosure.",
"type": "boolean"
},
"firstComment": {
"description": "For scheduled videos, posted at the scheduled time.",
"maxLength": 10000,
"type": "string"
},
"madeForKids": {
"default": false,
"type": "boolean"
},
"playlistId": {
"description": "From GET /api/v2/youtube/playlists for the same brand.",
"type": "string"
},
"tags": {
"description": "At most 500 characters combined.",
"items": {
"maxLength": 100,
"type": "string"
},
"type": "array"
},
"title": {
"description": "No < or >.",
"maxLength": 100,
"minLength": 1,
"type": "string"
},
"visibility": {
"default": "public",
"enum": [
"public",
"private",
"unlisted"
],
"type": "string"
}
},
"type": "object"
}
},
"required": [
"platforms"
],
"type": "object"
},
"name": "publish"
},
{
"description": "[read-only] Read-only analytics query: one `metric` aggregated (median by default) over posts, broken down by up to a few dimensions (platform, weekday, hour, brand, campaign, postType, label:<key>, arm), at a fixed post age `window` (48h / 7d / 30d / lifetime) so posts are compared fairly. Every row carries its sample size n; platforms that do not report the metric are listed in `availability`, never counted as 0. Pro plan or higher.\n(Wraps POST /api/v2/insights/query.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"aggregation": {
"default": "median",
"description": "Default median; pooled for the rates engagementRate, ctr and completionRate. A rate is pooled (Σ numerator ÷ Σ denominator), mean, median or count, never sum or p90. Pooled applies to rates only.",
"enum": [
"sum",
"mean",
"median",
"p90",
"count",
"pooled"
],
"type": "string"
},
"brandId": {
"description": "A brand id, or \"all\" (default: every profile you can see). A brand-bound key is always its own brand.",
"type": "string"
},
"breakdown": {
"description": "Up to 2 of: platform, brand, campaign, experiment, arm, weekday, hour, postType, or label:<key> (e.g. label:cut, label:line, label:source, label:hook, label:series). The multi-valued format key also splits per group: label:format@length, label:format@style, label:format@non_video, label:format@method (a post counts once in each, never twice in one). A post with no source: label (or no length) falls back to how it was made; those rows have `derived` > 0 and a \"derived\" label, and no label is written. \"arm\" = the experiment's compareKey and needs filters.experimentId.",
"items": {
"type": "string"
},
"maxItems": 2,
"type": "array"
},
"dateFrom": {
"description": "ISO date or epoch ms (publish time). Default: 90 days ago; ranges are capped at 400 days.",
"type": "string"
},
"dateTo": {
"description": "ISO date or epoch ms. Default: now.",
"type": "string"
},
"filters": {
"properties": {
"campaignId": {
"type": "string"
},
"excludeLabels": {
"description": "Posts with any of these labels are left out (the tag templates exclude line:test).",
"items": {
"type": "string"
},
"maxItems": 5,
"type": "array"
},
"experimentId": {
"description": "Reads the experiment over its own start–end window.",
"type": "string"
},
"labels": {
"description": "Every label must be on the post (key:value).",
"items": {
"type": "string"
},
"maxItems": 5,
"type": "array"
},
"platforms": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"format": {
"default": "json",
"description": "csv = the result as a spreadsheet-safe CSV attachment (UTF-8 BOM, CRLF, cells starting with = + - @ are prefixed with '). Without `format`, `Accept: text/csv` also selects CSV. Errors are always JSON.",
"enum": [
"json",
"csv"
],
"type": "string"
},
"formula": {
"description": "Optional (needs the Advanced insights feature): combine up to three series. Each series is a metric + aggregation over the same breakdown, window, interval, filters and range. `expression` (≤ 80 characters) uses A, B, C, numbers, + - * / and parentheses, e.g. \"A / B\". Rows are joined on their keys; a group missing from any series, or a division by zero, is null. n = the smallest n across series. `metric` defaults to (and must equal) series A's. Referral series can only be split by platform, weekday or hour.",
"properties": {
"expression": {
"maxLength": 80,
"type": "string"
},
"format": {
"default": "number",
"description": "percent = a ratio shown ×100 with a % sign.",
"enum": [
"number",
"percent"
],
"type": "string"
},
"series": {
"properties": {
"A": {
"properties": {
"aggregation": {
"enum": [
"sum",
"mean",
"median",
"p90",
"count",
"pooled"
],
"type": "string"
},
"metric": {
"enum": [
"impressions",
"views",
"engagement",
"engagementRate",
"clicks",
"ctr",
"reach",
"likes",
"comments",
"shares",
"saves",
"followersGained",
"watchTimeHours",
"avgWatchSeconds",
"completionRate",
"postsPublished",
"referralClicks",
"siteVisits",
"visitsPer1kReach"
],
"type": "string"
}
},
"required": [
"metric"
],
"type": "object"
},
"B": {
"properties": {
"aggregation": {
"enum": [
"sum",
"mean",
"median",
"p90",
"count",
"pooled"
],
"type": "string"
},
"metric": {
"enum": [
"impressions",
"views",
"engagement",
"engagementRate",
"clicks",
"ctr",
"reach",
"likes",
"comments",
"shares",
"saves",
"followersGained",
"watchTimeHours",
"avgWatchSeconds",
"completionRate",
"postsPublished",
"referralClicks",
"siteVisits",
"visitsPer1kReach"
],
"type": "string"
}
},
"required": [
"metric"
],
"type": "object"
},
"C": {
"properties": {
"aggregation": {
"enum": [
"sum",
"mean",
"median",
"p90",
"count",
"pooled"
],
"type": "string"
},
"metric": {
"enum": [
"impressions",
"views",
"engagement",
"engagementRate",
"clicks",
"ctr",
"reach",
"likes",
"comments",
"shares",
"saves",
"followersGained",
"watchTimeHours",
"avgWatchSeconds",
"completionRate",
"postsPublished",
"referralClicks",
"siteVisits",
"visitsPer1kReach"
],
"type": "string"
}
},
"required": [
"metric"
],
"type": "object"
}
},
"required": [
"A"
],
"type": "object"
}
},
"required": [
"series",
"expression"
],
"type": "object"
},
"interval": {
"description": "Adds a time series bucketed by publish date.",
"enum": [
"day",
"week",
"month"
],
"type": "string"
},
"metric": {
"default": "impressions",
"enum": [
"impressions",
"views",
"engagement",
"engagementRate",
"clicks",
"ctr",
"reach",
"likes",
"comments",
"shares",
"saves",
"followersGained",
"watchTimeHours",
"avgWatchSeconds",
"completionRate",
"postsPublished",
"referralClicks",
"siteVisits",
"visitsPer1kReach"
],
"type": "string"
},
"tzOffsetMinutes": {
"description": "Offset from UTC for weekday / hour / day buckets (e.g. -300).",
"type": "integer"
},
"window": {
"default": "7d",
"description": "Snapshot age: the metric as it stood 48h / 7d / 30d after publish, or the latest refresh.",
"enum": [
"48h",
"7d",
"30d",
"lifetime"
],
"type": "string"
}
},
"type": "object"
},
"name": "query_insights"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] `action`: \"skip\" passes over a queued item; \"retry\" puts a failed/skipped item back in line; \"unschedule\" deletes a booked platform post and puts the item back in line. Also `youtubeTitle`, per-item `platforms`, and `position` (0-based among waiting items). Needs the `publish` scope (or admin) on the Agency plan or higher.\n(Wraps PATCH /api/v2/queues/{id}/items/{itemId}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"action": {
"enum": [
"skip",
"retry",
"unschedule"
],
"type": "string"
},
"brandId": {
"type": "string"
},
"id": {
"type": "string"
},
"itemId": {
"type": "string"
},
"platforms": {
"description": "This item's own platforms, used when it is booked; null → back to the queue's. Only before the item is booked (queued / failed / skipped).",
"items": {
"type": "string"
},
"minItems": 1,
"type": [
"array",
"null"
]
},
"position": {
"description": "0-based index among the waiting (queued) items.",
"minimum": 0,
"type": "integer"
},
"youtubeTitle": {
"description": "null or \"\" → back to the draft's stored title.",
"maxLength": 100,
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"itemId"
],
"type": "object"
},
"name": "queue_item_action"
},
{
"description": "[DESTRUCTIVE] DESTRUCTIVE: removes an item (the draft stays in the library). A booked (`scheduled`) item is refused (409) unless `unschedule: true`, which deletes its platform post first. Needs the `publish` scope (or admin) on the Agency plan or higher.\n(Wraps DELETE /api/v2/queues/{id}/items/{itemId}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"description": "Account-wide keys: \"personal\" or an id from GET /api/v2/brands (omitted → the brand last active in the dashboard). A brand-bound key always uses its own brand.",
"type": "string"
},
"id": {
"type": "string"
},
"itemId": {
"type": "string"
},
"unschedule": {
"default": false,
"description": "true: delete the booked post at the platform first (the dashboard's \"Unschedule\"), then delete. Without it a booked post is a 409 — nothing is ever left booked at the platform without its queue item.",
"type": "boolean"
}
},
"required": [
"id",
"itemId"
],
"type": "object"
},
"name": "remove_queue_item"
},
{
"description": "[writes flayr data · `write` scope] Turns one Library image (the card) into a 6-15 s motion ad: hook (push-in on the card), up to 3 body beats (pans with the text), then a CTA end card in the brand color with the CTA and the link host. 9:16 unless `aspect` is 1:1 or 16:9. The text is used exactly as given; it is not rewritten. Saves a new draft in the card's brand (labels format:motion-ad, source:template); nothing is posted and the card is unchanged. Free. Returns a jobId: poll get_job_status.\n(Wraps POST /api/v2/video-studio/templates/motion-ad-from-card/render.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"aspect": {
"enum": [
"9:16",
"1:1",
"16:9"
],
"type": "string"
},
"body": {
"description": "Up to 3 beats are used; blanks are dropped.",
"items": {
"maxLength": 500,
"type": "string"
},
"maxItems": 10,
"type": "array"
},
"brandColor": {
"description": "#RRGGBB; defaults to the brand profile color.",
"maxLength": 20,
"type": "string"
},
"contentId": {
"description": "The card: a Library image.",
"type": "string"
},
"cta": {
"maxLength": 500,
"minLength": 1,
"type": "string"
},
"ctaUrl": {
"description": "http(s) link; its host is shown on the end card.",
"maxLength": 2048,
"minLength": 1,
"type": "string"
},
"hook": {
"maxLength": 500,
"minLength": 1,
"type": "string"
},
"textColor": {
"description": "#RRGGBB.",
"maxLength": 20,
"type": "string"
}
},
"required": [
"contentId",
"hook",
"cta",
"ctaUrl"
],
"type": "object"
},
"name": "render_motion_ad"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] Send every waiting (queued) item id exactly once, in the new order. Booked, posted, failed and skipped items keep their places. Needs the `publish` scope (or admin) on the Agency plan or higher.\n(Wraps POST /api/v2/queues/{id}/items/reorder.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"type": "string"
},
"id": {
"type": "string"
},
"itemIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"id",
"itemIds"
],
"type": "object"
},
"name": "reorder_queue_items"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] MOVES A SCHEDULED POST to `scheduledAt` (ISO 8601 with a timezone, e.g. \"2026-11-19T15:00:00Z\"; more than 5 minutes ahead). The booking keeps its id, caption, media, accounts and options; its posting-queue slot follows. Returns { status: \"rescheduled\", scheduledAt }. Refused within 5 minutes of the current slot, while it is posting or posted, and in a brand whose scheduled posts require review. Needs the `publish` scope (or admin) and the Pro plan or higher; each publish counts against the plan's direct posts for the month. Confirm the new time (and its timezone) with the user first.\n(Wraps PATCH /api/v2/posts/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"description": "The post: `convexPostId` from POST /api/v2/publish, or `posts[].id` from GET /api/v2/content/{id}.",
"type": "string"
},
"scheduledAt": {
"description": "Move the post to this time (ISO 8601 with Z or an offset, e.g. \"2026-11-19T15:00:00Z\"); more than 5 minutes in the future. Send it on its own: no other field with it.",
"format": "date-time",
"type": "string"
}
},
"required": [
"scheduledAt",
"id"
],
"type": "object"
},
"name": "reschedule_post"
},
{
"description": "[writes flayr data · `write` scope] Files the round's versions into the approval queue on the given platforms, in spaced slots with UTM links. Nothing posts until a person approves each one. Round 2 re-tests the arms kept from round 1.\n(Wraps POST /api/v2/experiments/{id}/rounds.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"platforms": {
"items": {
"type": "string"
},
"maxItems": 10,
"minItems": 1,
"type": "array"
},
"startAt": {
"description": "First slot; default now.",
"oneOf": [
{
"format": "date-time",
"type": "string"
},
{
"type": "number"
}
]
}
},
"required": [
"id",
"platforms"
],
"type": "object"
},
"name": "start_experiment_round"
},
{
"description": "[read-only] Hashtag and @mention suggestions for each platform, from the brand's own config: hashtags from its brand profile's hashtag sets (within each platform's limit, skipping ones already in `text`), mentions from its mention contacts written the way each network receives them (`@handle`; on LinkedIn `@[Name](urn:li:organization:…)`). A contact with no account on a platform is not suggested there. Suggestions only: nothing is added to any caption; offer them to the user. Instagram allows 30 hashtags and YouTube 15; publish removes extra ones.\n(Wraps POST /api/v2/content/suggest-tags.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"platforms": {
"items": {
"enum": [
"instagram",
"tiktok",
"youtube",
"youtube_shorts",
"linkedin",
"twitter",
"threads",
"bluesky",
"facebook"
],
"type": "string"
},
"minItems": 1,
"type": "array"
},
"text": {
"description": "The caption so far; tags and mentions already in it are not suggested again.",
"maxLength": 10000,
"type": "string"
}
},
"required": [
"platforms"
],
"type": "object"
},
"name": "suggest_tags"
},
{
"description": "[writes flayr data · `write` scope] Edits a draft in place; only the fields you pass change. Any media field replaces the draft's media as a set (keeps the id, so queue items still point at it). Refused (409) for archived or already-published items, and a media swap is refused once a queue booked it. `labels`, `opus`, `videoWatermarked`, `library` and `aiGenerated` may be changed on published items too. `aiGenerated` is the post's AI label: false for real footage, null back to automatic. A video draft's custom thumbnail/cover: `thumbnailStorageId` (an image from upload_media) or `thumbnailUrl`, or `thumbnailFrameOffsetMs` for a frame of the video; `thumbnailUrl: null` clears it. It is sent to YouTube (long-form), Facebook, LinkedIn, Instagram Reels, TikTok and Pinterest at publish.\n(Wraps PATCH /api/v2/content/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"aiGenerated": {
"description": "The post's \"AI-generated\" label toggle: false = real footage (no AI label), true = always label, null = automatic (on when flayr made the media). Metadata: allowed on published items. Removing the `source:ai-generated` label alone does not turn it off.",
"type": [
"boolean",
"null"
]
},
"credits": {
"description": "Creator credits for work this post uses. Added to the published caption above the flayr footer, one line each: model → \"Print your own: <url> (model by <author>)\"; other kinds → \"Credit: <title> by <author> <url>\". Never added twice (a URL already in the caption is skipped); on X a shorter line (or no URL) is used, or the credits are left out, before the caption text is ever cut. YouTube: description only, never the title.",
"items": {
"additionalProperties": false,
"properties": {
"author": {
"description": "Designer / artist / photographer.",
"maxLength": 200,
"type": "string"
},
"kind": {
"enum": [
"model",
"music",
"footage",
"image",
"other"
],
"type": "string"
},
"site": {
"description": "Detected from the URL when omitted (e.g. MakerWorld, Printables, Thingiverse).",
"maxLength": 200,
"type": "string"
},
"title": {
"maxLength": 200,
"type": "string"
},
"url": {
"description": "Absolute http(s) link to the credited work.",
"format": "uri",
"maxLength": 2048,
"type": "string"
}
},
"required": [
"kind",
"url"
],
"type": "object"
},
"maxItems": 10,
"type": [
"array",
"null"
]
},
"id": {
"type": "string"
},
"imageStorageId": {
"description": "Uploaded image storageId: the video's thumbnail, or on its own the post's single image. Not with imageUrl.",
"type": "string"
},
"imageUrl": {
"description": "http(s) image URL: the video's thumbnail, or on its own the post's single image. Not with imageStorageId.",
"format": "uri",
"type": "string"
},
"images": {
"description": "Carousel image URLs. Cannot be combined with the other media fields.",
"items": {
"format": "uri",
"type": "string"
},
"maxItems": 20,
"minItems": 2,
"type": "array"
},
"labels": {
"description": "Replaces the labels ([] or null clears). Allowed on published items too. Normalized \"key:value\" labels (keys and values are lower-cased, spaces become \"-\"). Linked keys: campaign:<media campaign id>, experiment:<experiment id> (+ variant:<name> or the experiment's compareKey), type:<post type> (short, long-form, carousel, image, text, reel, story, poem, timelapse…). Free keys work too: cut:opus, lane:tech, parent:pantheon-part-1, ab:<pair id> (an A/B pair). Labels are copied onto every post made from the draft, and compared in GET /api/v2/analytics/experiments.",
"items": {
"pattern": "^[a-z0-9][a-z0-9_-]{0,31}:[a-z0-9][a-z0-9._\\-/+]{0,79}$",
"type": "string"
},
"maxItems": 30,
"type": [
"array",
"null"
]
},
"library": {
"additionalProperties": false,
"description": "The Library item's own title and description, searchable from the Library. null or \"\" clears one. Metadata: allowed on published items. Not the same as `title` (the YouTube video title).",
"minProperties": 1,
"properties": {
"description": {
"maxLength": 2000,
"type": [
"string",
"null"
]
},
"title": {
"maxLength": 120,
"type": [
"string",
"null"
]
}
},
"type": "object"
},
"opus": {
"additionalProperties": false,
"description": "Link this row to an Opus Clip clip (owner only; allowed on published rows) so an Opus project import skips it instead of creating a duplicate. null unlinks.",
"properties": {
"clipId": {
"description": "Opus clip id: the full \"{projectId}.{curationId}\" or the short curation id with projectId.",
"type": "string"
},
"projectId": {
"description": "Opus project id. Optional when clipId is the full \"{projectId}.{clipId}\" id.",
"type": "string"
}
},
"required": [
"clipId"
],
"type": [
"object",
"null"
]
},
"platformText": {
"additionalProperties": {
"maxLength": 10000,
"type": [
"string",
"null"
]
},
"description": "Per-platform captions: platform id → the caption that platform gets instead of `text`, e.g. { \"twitter\": \"short version\" }. Each must fit its platform: X 280 (a link counts 23, an emoji 2), Bluesky 300, Threads 500, Instagram 2200, LinkedIn 3000. Platforms without one get `text` when it fits, else an automatic short version: the first line, the credits in short form, as many hashtags as fit and the short flayr link (hashtags go first, then the credit link becomes \"Model by <author>\"; words are never cut). PATCH merges: a null (or \"\") value clears that platform, platformText: null clears all. At publish every platform is validated on its own caption: one failing platform no longer fails the others — the post publishes to the rest and reports the failures in platformErrors.",
"type": [
"object",
"null"
]
},
"platforms": {
"items": {
"type": "string"
},
"type": "array"
},
"text": {
"maxLength": 10000,
"minLength": 1,
"type": "string"
},
"thumbnailFrameOffsetMs": {
"description": "Use this frame of the video (ms from the start) as the cover. Instagram and TikTok get the offset; YouTube/Facebook/LinkedIn need an image, so pass the captured frame as thumbnailStorageId / thumbnailUrl too.",
"maximum": 86400000,
"minimum": 0,
"type": "integer"
},
"thumbnailStorageId": {
"description": "Custom video thumbnail: an image storageId from POST /api/v2/media/upload-url. Not with thumbnailUrl. null clears.",
"type": [
"string",
"null"
]
},
"thumbnailUrl": {
"description": "Custom video thumbnail: an http(s) image URL. Not with thumbnailStorageId. null clears.",
"format": "uri",
"type": [
"string",
"null"
]
},
"title": {
"description": "Shorthand for youtube.title (the video title). No < or >.",
"maxLength": 100,
"minLength": 1,
"type": "string"
},
"videoStorageId": {
"description": "storageId from uploading to POST /api/v2/media/upload-url's uploadUrl. Not with videoUrl. Another account's upload is refused (422).",
"type": "string"
},
"videoUrl": {
"description": "http(s) URL of the new video. Not with videoStorageId.",
"format": "uri",
"type": "string"
},
"videoWatermarked": {
"additionalProperties": false,
"description": "The item's own video already has the \"Posted by flayr.social\" watermark: publish it as-is. null clears.",
"properties": {
"at": {
"description": "When it was watermarked (epoch ms). Default: now.",
"type": "integer"
},
"version": {
"maximum": 1,
"minimum": 1,
"type": "integer"
}
},
"required": [
"version"
],
"type": [
"object",
"null"
]
},
"youtube": {
"description": "Merged into the stored YouTube options. YouTube upload options, applied to YouTube targets only. madeForKids is always sent to YouTube (default false). A missing title is derived from the first sentence of the caption, cut to 100 characters at a word boundary. SCHEDULED UPLOADS: the video is uploaded ahead of scheduledAt. \"public\" is uploaded as private and released by YouTube at the scheduled time; \"unlisted\" is watchable by anyone with the link from upload time; \"private\" stays private.",
"properties": {
"categoryId": {
"description": "YouTube category id, e.g. \"28\" (Science & Technology). YouTube default: 22.",
"pattern": "^\\d{1,3}$",
"type": "string"
},
"containsSyntheticMedia": {
"description": "AI-generated content disclosure.",
"type": "boolean"
},
"firstComment": {
"description": "For scheduled videos, posted at the scheduled time.",
"maxLength": 10000,
"type": "string"
},
"madeForKids": {
"default": false,
"type": "boolean"
},
"playlistId": {
"description": "From GET /api/v2/youtube/playlists for the same brand.",
"type": "string"
},
"tags": {
"description": "At most 500 characters combined.",
"items": {
"maxLength": 100,
"type": "string"
},
"type": "array"
},
"title": {
"description": "No < or >.",
"maxLength": 100,
"minLength": 1,
"type": "string"
},
"visibility": {
"default": "public",
"enum": [
"public",
"private",
"unlisted"
],
"type": "string"
}
},
"type": "object"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "update_content"
},
{
"description": "[writes flayr data · `write` scope] Changes an experiment's fields (e.g. status \"running\" / \"done\", endAt, notes). Only the fields you pass change.\n(Wraps PATCH /api/v2/experiments/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"compareKey": {
"type": "string"
},
"endAt": {
"format": "date-time",
"type": [
"string",
"null"
]
},
"hypothesis": {
"maxLength": 2000,
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"maxLength": 120,
"type": "string"
},
"notes": {
"maxLength": 2000,
"type": "string"
},
"primaryMetric": {
"enum": [
"impressions",
"views",
"engagement",
"engagementRate",
"clicks"
],
"type": "string"
},
"startAt": {
"format": "date-time",
"type": [
"string",
"null"
]
},
"status": {
"enum": [
"draft",
"running",
"done"
],
"type": "string"
},
"variants": {
"items": {
"type": "string"
},
"maxItems": 12,
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "update_experiment"
},
{
"description": "[writes flayr data · `write` scope] Frame every image posted to a platform automatically (frameRef), or stop (frameRef null). Applies to posts published or scheduled from now on. Confirm with the user first.\n(Wraps POST /api/v2/frames/settings.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"aspect": {
"enum": [
"1:1",
"4:5",
"9:16",
"16:9"
],
"type": "string"
},
"frameRef": {
"type": [
"string",
"null"
]
},
"platform": {
"type": "string"
}
},
"required": [
"platform",
"frameRef"
],
"type": "object"
},
"name": "update_frame_settings"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] Changes a trigger; only the fields you pass change. `active: true` makes it reply to and DM real people from then on and needs the publish scope: confirm with the user first. `active: false` pauses it.\n(Wraps PATCH /api/v2/growth-triggers/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"accountId": {
"description": "A connected account id (GET /api/v2/accounts); omitted = every account of the brand.",
"type": "string"
},
"active": {
"type": "boolean"
},
"brandId": {
"description": "A brand id (GET /api/v2/brands) or \"personal\"; default: the key owner's active brand. A brand-bound key is always pinned to its brand; any other value is 403.",
"type": "string"
},
"campaignKey": {
"description": "utm_campaign for {{link}}, and the campaign column of the logs.",
"type": "string"
},
"delaySeconds": {
"description": "Wait before replying; capped at 20 at send time.",
"minimum": 0,
"type": "number"
},
"dmText": {
"description": "Private reply (Instagram and Facebook only; refused on an account of another platform).",
"type": "string"
},
"dmVariations": {
"items": {
"type": "string"
},
"type": "array"
},
"id": {
"type": "string"
},
"keywords": {
"description": "Case-insensitive. exact = the whole comment; startsWith = its start; contains = anywhere (substring, so \"hi\" also matches \"this\").",
"items": {
"properties": {
"matchType": {
"enum": [
"exact",
"contains",
"startsWith"
],
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"value",
"matchType"
],
"type": "object"
},
"minItems": 1,
"type": "array"
},
"link": {
"description": "http(s) URL behind {{link}}.",
"format": "uri",
"type": "string"
},
"maxSends": {
"description": "After this many sends, reply only overCapText.",
"minimum": 1,
"type": "integer"
},
"name": {
"type": "string"
},
"overCapText": {
"type": "string"
},
"postIds": {
"description": "Only comments on these posts; omitted = all posts.",
"items": {
"type": "string"
},
"type": "array"
},
"priority": {
"description": "Higher is checked first; the first matching trigger wins.",
"type": "number"
},
"repeatPolicy": {
"default": "once_per_commenter",
"enum": [
"once_per_commenter",
"every_comment"
],
"type": "string"
},
"replyText": {
"description": "Public reply. {{name}} = the commenter, {{link}} = `link` with UTM tags. Must not contain a keyword (it would trigger itself).",
"type": "string"
},
"replyVariations": {
"description": "Alternatives picked at random with replyText.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "update_growth_trigger"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] `status: \"paused\"` stops new bookings (already-booked posts stay booked); \"active\" resumes. Other fields (platforms, cadenceHours, timeOfDay, lookahead, startsAt, name) apply from the next booking. Needs the `publish` scope (or admin) on the Agency plan or higher.\n(Wraps PATCH /api/v2/queues/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"brandId": {
"type": "string"
},
"cadenceHours": {
"description": "1, 2, 3, 4, 6, 8, 12, or whole days (24, 48, …, 720).",
"type": "integer"
},
"id": {
"type": "string"
},
"lookahead": {
"maximum": 10,
"minimum": 1,
"type": "integer"
},
"name": {
"maxLength": 80,
"type": "string"
},
"platforms": {
"description": "Replaces the queue's platforms for items booked from now on. Already-booked items keep theirs; items with their own `platforms` keep using them.",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"startsAt": {
"description": "ISO 8601 with an offset (or Z). Nothing is booked before it; the first slot is the first timeOfDay (in the queue's timezone) at or after it, so a startsAt that is itself a slot is the first slot. A past date holds nothing back. null clears it.",
"format": "date-time",
"type": [
"string",
"null"
]
},
"status": {
"enum": [
"active",
"paused"
],
"type": "string"
},
"timeOfDay": {
"description": "24-hour \"HH:MM\" local time.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "update_queue"
},
{
"description": "[PUBLISHES to social accounts · `publish` scope] CHANGES WHAT A SCHEDULED POST WILL PUBLISH on real accounts, keeping its slot, accounts, options, labels and draft. `firstComment` sets per-platform first comments (linkedin, facebook, instagram, threads, youtube; null removes; X/Bluesky have none); `text` / `platformText` change the caption; `applyAutoFirstComment: true` adds the brand's auto first comment where none is set; `removePlatforms` drops platforms from this booking (e.g. a duplicate TikTok); `labels` replaces the post's own labels (e.g. [\"repost:evergreen\"]; alone it never touches the booking, status \"labeled\"). Updated in place when the platform scheduler allows it, otherwise re-booked at the same slot (status \"rebooked\", new postId). Refused within 5 minutes of the slot. Repeating the same edit returns \"unchanged\". Needs the `publish` scope (or admin) and the Pro plan or higher; each publish counts against the plan's direct posts for the month. Confirm the new text with the user first.\n(Wraps PATCH /api/v2/posts/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"applyAutoFirstComment": {
"description": "Add the brand's auto first comment where the post has none.",
"type": "boolean"
},
"firstComment": {
"additionalProperties": false,
"description": "The first comment the post publishes, per platform: a string sets it, null or \"\" removes it; platforms not named keep theirs. X and Bluesky have no first comment at the platform scheduler and are refused (422). A Facebook / Instagram Story never gets one. flayr's own \"Posted with flayr.social\" line on a comment is kept. Also saved on the post's draft, so a later edit never puts the old one back.",
"minProperties": 1,
"properties": {
"facebook": {
"maxLength": 8000,
"type": [
"string",
"null"
]
},
"instagram": {
"maxLength": 2200,
"type": [
"string",
"null"
]
},
"linkedin": {
"maxLength": 1250,
"type": [
"string",
"null"
]
},
"threads": {
"maxLength": 500,
"type": [
"string",
"null"
]
},
"youtube": {
"maxLength": 10000,
"type": [
"string",
"null"
]
}
},
"type": "object"
},
"id": {
"description": "The post: `convexPostId` from POST /api/v2/publish, or `posts[].id` from GET /api/v2/content/{id}.",
"type": "string"
},
"instanceLabels": {
"description": "Internal labels per platform/account instance. An array of {platform, accountId?, labels}; accountId is the Zernio connected account id. Omit accountId when unknown. instanceKey is derived as canonical platform + \":account:\" + URL-encoded account id, or \":pending\" when unknown (twitter canonicalizes to x). Replaces the instance set when editing; [] clears it. Never published.",
"items": {
"additionalProperties": false,
"properties": {
"accountId": {
"maxLength": 200,
"type": "string"
},
"labels": {
"items": {
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"platform": {
"type": "string"
}
},
"required": [
"platform",
"labels"
],
"type": "object"
},
"maxItems": 20,
"type": "array"
},
"labelMode": {
"description": "Default set. add atomically appends labels to the post and matching instances; other instances stay unchanged.",
"enum": [
"set",
"add"
],
"type": "string"
},
"labels": {
"description": "Sets this post's own labels ([] or null clears; labelMode add appends), e.g. [\"repost:evergreen\"]. flayr metadata: sent alone it never touches the booking (status \"labeled\") and works on posted posts too. With booking edits, metadata is saved after a successful edit. Normalized \"key:value\" labels (keys and values are lower-cased, spaces become \"-\"). Linked keys: campaign:<media campaign id>, experiment:<experiment id> (+ variant:<name> or the experiment's compareKey), type:<post type> (short, long-form, carousel, image, text, reel, story, poem, timelapse…). Free keys work too: cut:opus, lane:tech, parent:pantheon-part-1, ab:<pair id> (an A/B pair). Labels are copied onto every post made from the draft, and compared in GET /api/v2/analytics/experiments.",
"items": {
"pattern": "^[a-z0-9][a-z0-9_-]{0,31}:[a-z0-9][a-z0-9._\\-/+]{0,79}$",
"type": "string"
},
"maxItems": 30,
"type": [
"array",
"null"
]
},
"platformText": {
"additionalProperties": {
"type": [
"string",
"null"
]
},
"description": "Per-platform captions, merged into the draft's: { \"linkedin\": \"…\" } sets one, null / \"\" clears it; null clears them all.",
"type": [
"object",
"null"
]
},
"removePlatforms": {
"description": "Platform ids to drop from this booking (e.g. a duplicate [\"tiktok\"]). Each must be on the post; dropping every platform is refused (use DELETE). The booking is rebuilt from the draft.",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"scheduledAt": {
"description": "Move the post to this time (ISO 8601 with Z or an offset, e.g. \"2026-11-19T15:00:00Z\"); more than 5 minutes in the future. Send it on its own: no other field with it.",
"format": "date-time",
"type": "string"
},
"text": {
"description": "The caption (every platform without its own).",
"maxLength": 10000,
"minLength": 1,
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "update_scheduled_post"
},
{
"description": "[writes flayr data · `write` scope] Changes a format; only the fields you pass change. `archived: true` hides it.\n(Wraps PATCH /api/v2/text-templates/{id}.)",
"inputSchema": {
"additionalProperties": false,
"properties": {
"archived": {
"type": "boolean"
},
"ctaSlots": {
"items": {
"properties": {
"label": {
"type": "string"
},
"placement": {
"default": "body",
"enum": [
"body",
"first_comment",
"none"
],
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"label"
],
"type": "object"
},
"maxItems": 10,
"type": "array"
},
"defaultLabels": {
"description": "\"key:value\" labels every draft started from this format gets (plus the format's own `label`).",
"items": {
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"example": {
"maxLength": 20000,
"type": [
"string",
"null"
]
},
"format": {
"enum": [
"post",
"article",
"carousel_script",
"meme",
"thread"
],
"type": "string"
},
"guardrails": {
"items": {
"maxLength": 500,
"type": "string"
},
"maxItems": 30,
"type": "array"
},
"hookPatterns": {
"description": "\"[slot]\" placeholders are kept as written.",
"items": {
"maxLength": 300,
"type": "string"
},
"maxItems": 20,
"type": "array"
},
"id": {
"type": "string"
},
"length": {
"properties": {
"captionWords": {
"description": "carousel_script: the caption that goes with the slides.",
"properties": {
"max": {
"type": "integer"
},
"min": {
"type": "integer"
}
},
"type": "object"
},
"max": {
"minimum": 1,
"type": "integer"
},
"min": {
"minimum": 0,
"type": "integer"
},
"perSlideMaxWords": {
"description": "carousel_script / thread: words allowed per slide or post.",
"type": "integer"
},
"titleMaxWords": {
"description": "carousel_script: words allowed in the title (first line of slide 1).",
"type": "integer"
},
"unit": {
"enum": [
"words",
"chars",
"slides"
],
"type": "string"
}
},
"required": [
"unit",
"min",
"max"
],
"type": "object"
},
"maxHashtags": {
"maximum": 30,
"minimum": 0,
"type": [
"integer",
"null"
]
},
"name": {
"maxLength": 120,
"type": "string"
},
"platform": {
"description": "e.g. \"linkedin\"; null = any platform.",
"type": [
"string",
"null"
]
},
"sections": {
"description": "In order. \"Start from a format\" writes one `[you fill: Label — guidance]` placeholder per section.",
"items": {
"properties": {
"guidance": {
"maxLength": 1000,
"type": "string"
},
"key": {
"description": "Derived from the label when omitted.",
"type": "string"
},
"label": {
"maxLength": 80,
"type": "string"
},
"maxLines": {
"type": "integer"
},
"minLines": {
"type": "integer"
},
"required": {
"default": true,
"type": "boolean"
}
},
"required": [
"label"
],
"type": "object"
},
"maxItems": 20,
"type": "array"
},
"source": {
"description": "Where the shape came from, e.g. an import path.",
"maxLength": 200,
"type": [
"string",
"null"
]
},
"useFor": {
"description": "One line: when to pick this format.",
"maxLength": 300,
"type": [
"string",
"null"
]
}
},
"required": [
"id"
],
"type": "object"
},
"name": "update_text_template"
},
{
"description": "[writes flayr data · `write` scope] Step 1 of attaching a LOCAL file (video or image) to a draft. Returns a single-use, short-lived `uploadUrl`. Step 2: POST the raw file bytes to it with the file's Content-Type, e.g. `curl -X POST -H \"Content-Type: video/mp4\" --data-binary @clip.mp4 \"<uploadUrl>\"` — the JSON response has a `storageId`. Step 3: pass it as `videoStorageId` or `imageStorageId` on create_content or update_content. For media already on the web, skip this and pass `videoUrl` / `imageUrl` instead. Returns 413 when the account's storage is full.\n(Wraps POST /api/v2/media/upload-url.)",
"inputSchema": {
"additionalProperties": false,
"properties": {},
"type": "object"
},
"name": "upload_media"
},
{
"description": "[read-only] Returns who this connection (an OAuth sign-in or an API key) belongs to: userId, the account's current plan tier, and brandId/brandName when the connection is bound to one brand (null = account-wide). Call this first to learn what the connection can reach.\n(Wraps GET /api/v2/ping.)",
"inputSchema": {
"additionalProperties": false,
"properties": {},
"type": "object"
},
"name": "whoami"
}
]
}What the leaf hashes
{"body":"1bc4966bb3c588a0ccd6d0281628220d52f542ec60ce1a49b1e181f53523356a","id":4360747,"kind":"mcp.tools","method":"probe.mcp/1","observed_us":1794206384731565,"prev":4139974,"subject":"b7b10d45-f6fb-49e6-abb9-091c4a879468","subject_kind":"endpoint"}Proof
Its day has not closed yet. A day closes ten minutes after midnight UTC; the proof appears here then.