← The evidence ledger
Evidence row 1226289
Observed , by probe.mcp/1.
- Kind
- mcp.tools
- About
- endpoint b7b10d45-f6fb-49e6-abb9-091c4a879468
- Recorded
- Previous row
- None: the first of its kind about this subject
- Body SHA-256
- b715ce9da6d51d9b61a6a9a8ac702912c6582cabec21462eafa36c0de4cfdc24
- Leaf
- 0xa3df3afc50f77b0ae35f09dde3a06829c2fff638f6682d710bb58cb3149ee45e
What was observed
{
"count": 57,
"tools": [
{
"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": "[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"
},
"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 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": "[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": "[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": "[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"
},
"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": "[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": "[PUBLISHES to social accounts · `publish` scope] Same rules as the dashboard: only the assigned approver 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. 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": {
"maxLength": 1000,
"type": "string"
}
},
"required": [
"id",
"decision"
],
"type": "object"
},
"name": "decide_approval"
},
{
"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 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": "[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] 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] 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] 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] Status of an async job (combine_clips, or an API video generation): pending, processing, completed or failed. For a combine, `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, 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": "[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\". 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"
],
"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).\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] 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": "[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. 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). `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. `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.",
"properties": {
"contentType": {
"enum": [
"reel",
"story",
"feed"
],
"type": "string"
}
},
"type": "object"
},
"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"
},
"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"
},
"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 (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"
],
"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"
],
"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"
],
"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"
],
"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": "[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": "[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` and `videoWatermarked` may be changed on published items too. 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": {
"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"
]
},
"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] `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). 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"
},
"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"
},
"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":"b715ce9da6d51d9b61a6a9a8ac702912c6582cabec21462eafa36c0de4cfdc24","id":1226289,"kind":"mcp.tools","method":"probe.mcp/1","observed_us":1793332981459536,"prev":null,"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.