{
  "openapi": "3.1.0",
  "info": {
    "title": "Looptake API",
    "version": "1.0.0",
    "summary": "Describe a video. Get one built to go viral.",
    "description": "The public REST API for Looptake. Every screen in the product is one of these endpoints.\n\nAuthenticate with `Authorization: Bearer lt_live_...` or `X-API-Key`.\n\nSuccess responses are `{ \"success\": true, \"data\": ... }`, optionally with `pagination`. Failures are `{ \"success\": false, \"error\": { \"type\", \"message\" } }`.\n\nSend `Idempotency-Key` on any POST that spends credits or publishes. Retrying with the same key and body replays the stored response, so a network timeout cannot double-post.",
    "contact": {
      "name": "Looptake support",
      "email": "support@looptake.com",
      "url": "https://looptake.com/developers"
    },
    "termsOfService": "https://looptake.com/terms"
  },
  "servers": [
    {
      "url": "https://looptake.com/api/v1",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "me",
      "description": "Identity. What this key can do, which team it belongs to, and how many credits that team has left."
    },
    {
      "name": "credits",
      "description": "Credits. The balance and every movement in it, including the automatic refunds for failed generations."
    },
    {
      "name": "workspaces",
      "description": "Workspaces. A workspace is a brand. Everything with content in it is workspace-scoped."
    },
    {
      "name": "accounts",
      "description": "Social accounts. The TikTok, Instagram and YouTube accounts connected over OAuth, and whether their tokens are still good."
    },
    {
      "name": "generations",
      "description": "Generations. Suggestions: a caption, a post body, hashtags, a rationale and a deck. Accepting one turns it into content."
    },
    {
      "name": "posts",
      "description": "Posts and calendar. Scheduling and publishing, with one result row per platform account and the analytics that came back."
    },
    {
      "name": "automations",
      "description": "Automations. A cadence that fills a calendar. Draft, generate, review, launch, and the posts it produced."
    },
    {
      "name": "influencers",
      "description": "Influencers. AI presenters you create once and reuse, and the talking-head videos rendered from them."
    },
    {
      "name": "assets",
      "description": "Assets. The workspace library: clips, images, audio and fonts a deck can draw on."
    },
    {
      "name": "content",
      "description": "Content. Render status and the finished media for an accepted suggestion or a studio job."
    },
    {
      "name": "studio",
      "description": "Content studio. The content studio renders, one endpoint per named render. All accept a job and return 202; none blocks."
    },
    {
      "name": "analytics",
      "description": "Analytics. Performance by window and by post, from the platforms’ own reporting APIs."
    },
    {
      "name": "trends",
      "description": "Trends. The daily trend library: sounds, formats, topics and hashtags, per platform."
    },
    {
      "name": "webhooks",
      "description": "Webhooks. Four events pushed to a URL you own, so nothing has to poll a schedule that runs for a month."
    },
    {
      "name": "brand",
      "description": "Brand context. Re-read the website and rebuild the brand context every generation is written from."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Introspect the key.",
        "description": "Returns the team the key belongs to, the plan, the credit balance, this period’s plan-quota usage, the default workspace and the scopes the key carries. This is the only endpoint that requires no scope, which makes it the right health check: a key that cannot call it is not a key.\n\nThe key itself is never returned. Only its public id, its name and its scopes.\n\nusage.metrics carries one row per metered plan line, whether or not it has moved this period, so four meters can be rendered on the first day of the month. limit and remaining are both null on a plan where the line is unlimited: render the word rather than a number that means it. These are the same counters that answer 429 usage_limit_reached, and remaining is measured against the same ceiling that refusal is checked against.\n\nScope: `any`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "me"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "team": {
                      "id": "e7293116-fd57-4456-8da0-032f4976145f",
                      "name": "Northwind Labs",
                      "plan": "creator"
                    },
                    "credits": {
                      "balance": 268,
                      "grantedThisPeriod": 300,
                      "periodEnd": "2026-09-01T00:00:00Z"
                    },
                    "usage": {
                      "periodStart": "2026-08-01",
                      "metrics": [
                        {
                          "metric": "turbo_posts",
                          "used": 291,
                          "limit": 300,
                          "remaining": 9
                        },
                        {
                          "metric": "content_pieces",
                          "used": 12,
                          "limit": 60,
                          "remaining": 48
                        },
                        {
                          "metric": "automations",
                          "used": 1,
                          "limit": 5,
                          "remaining": 4
                        },
                        {
                          "metric": "generations",
                          "used": 140,
                          "limit": 300,
                          "remaining": 160
                        }
                      ]
                    },
                    "defaultWorkspace": {
                      "id": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                      "name": "Northwind",
                      "websiteUrl": "https://northwind.example"
                    },
                    "key": {
                      "id": "d81ed241-9404-4a1a-8864-242ecc540416",
                      "name": "CI publisher",
                      "scopes": [
                        "generations:read",
                        "generations:write",
                        "posts:read",
                        "posts:write"
                      ],
                      "createdAt": "2026-07-02T09:14:00Z",
                      "lastUsedAt": "2026-08-12T11:02:41Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/credits": {
      "get": {
        "operationId": "getCredits",
        "summary": "Read the balance and the ledger.",
        "description": "The balance is the team’s, not the workspace’s, because credits are billed at the team level. The ledger is every movement in signed units: a grant is positive, a spend is negative, and a refund for a failed generation is a positive entry that names the job it reverses.\n\nNewest first. The ledger is append-only; an entry is never edited or removed.\n\n`model` is reserved for the model that produced the media. It is null on every row until a media provider reports one, so do not build cost attribution on it yet.\n\nScope: `credits:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "credits"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Filters the ledger to one kind of movement.",
            "schema": {
              "type": "string",
              "enum": [
                "recurring",
                "topup",
                "spending",
                "trial",
                "spin",
                "reward",
                "refund",
                "carryover",
                "carry_expired"
              ],
              "description": "Filters the ledger to one kind of movement."
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "One-based page number.",
            "schema": {
              "type": "integer",
              "description": "One-based page number.",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "description": "Items per page.",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "balance": 268,
                    "ledger": [
                      {
                        "id": "f4451e86-4d4f-409f-aefb-02e1ca406365",
                        "credits": 10,
                        "type": "refund",
                        "spendingType": "studio_render",
                        "model": null,
                        "refId": "c02119cb-ed7a-40ed-87fe-c18edc1fa1a2",
                        "createdAt": "2026-08-11T18:22:07Z"
                      },
                      {
                        "id": "87d5ca77-433e-4ffc-870a-b33e3c2d4c76",
                        "credits": 30,
                        "type": "refund",
                        "spendingType": "influencer_video_per_second",
                        "model": null,
                        "refId": "08363fcd-f524-4126-b68d-fd64f2f9c09e",
                        "createdAt": "2026-08-04T07:41:55Z"
                      }
                    ]
                  },
                  "pagination": {
                    "total": 6,
                    "page": 1,
                    "limit": 2,
                    "pages": 3
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/workspaces": {
      "get": {
        "operationId": "getWorkspaces",
        "summary": "List the team’s workspaces.",
        "description": "One entry per brand, **archived ones included**, live first. `archivedAt` is null on a workspace that still accepts work and an instant on one that does not. An archived workspace is listed because its id is what `POST /workspaces/{id}/restore` needs. `brandRefreshedAt` is when the site was last crawled and the brand context rebuilt; a null means the workspace has a URL but has not been analysed yet.\n\nOrdered live first, then the default, then oldest first. A key pinned to one workspace sees only that one, archived or not.\n\nScope: `workspaces:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "workspaces"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                      "name": "Northwind",
                      "websiteUrl": "https://northwind.example",
                      "logoUrl": "https://cdn.looptake.com/91e14cbf-2c80-4b3b-a6bf-776bbde6b54c/logo.png",
                      "isDefault": true,
                      "archivedAt": null,
                      "brandRefreshedAt": "2026-08-09T06:00:12Z",
                      "createdAt": "2026-07-01T13:55:00Z"
                    },
                    {
                      "id": "d0e2f3a4-5b6c-4d7e-8f90-1a2b3c4d5e6f",
                      "name": "Northwind Trade",
                      "websiteUrl": null,
                      "logoUrl": null,
                      "isDefault": false,
                      "archivedAt": "2026-08-10T09:12:44Z",
                      "brandRefreshedAt": null,
                      "createdAt": "2026-07-14T08:30:00Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postWorkspaces",
        "summary": "Create a workspace.",
        "description": "Creating a workspace with a `websiteUrl` queues the first crawl, so the brand context is being built by the time the response arrives. The plan caps how many workspaces a team may hold; the cap is a `429 usage_limit_reached`, not a 400.\n\nThe cap counts live workspaces only, so archiving one frees its slot and restoring one needs a free slot.\n\nScope: `workspaces:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "workspaces"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "What the brand is called. Shown everywhere the workspace is chosen."
                  },
                  "websiteUrl": {
                    "type": "string",
                    "description": "The site to read the brand context from. Queues a crawl when present.",
                    "format": "uri"
                  },
                  "logoUrl": {
                    "type": "string",
                    "description": "Overrides the logo found on the site.",
                    "format": "uri"
                  }
                },
                "required": [
                  "name"
                ],
                "additionalProperties": false
              },
              "example": {
                "name": "Northwind",
                "websiteUrl": "https://northwind.example"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                    "name": "Northwind",
                    "websiteUrl": "https://northwind.example",
                    "logoUrl": null,
                    "isDefault": false,
                    "archivedAt": null,
                    "brandRefreshedAt": null,
                    "createdAt": "2026-08-12T11:04:20Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/workspaces/{id}": {
      "get": {
        "operationId": "getWorkspace",
        "summary": "Read a workspace and its brand context.",
        "description": "The brand context is what every generation is written from: identity, audience, tone, value propositions, content angles, keywords, competitors and palette. Each extracted claim carries the source URL it was read from, so nothing in it is unattributable. An archived workspace answers `200` here with `archivedAt` set, because the read is what a restore starts from.\n\nScope: `workspaces:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "workspaces"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workspace id.",
            "schema": {
              "type": "string",
              "description": "The workspace id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                    "name": "Northwind",
                    "websiteUrl": "https://northwind.example",
                    "isDefault": true,
                    "archivedAt": null,
                    "brandContext": {
                      "identity": "Inventory forecasting for independent retailers.",
                      "audience": "Owner-operators running one to five shops.",
                      "tone": "Direct, numerate, no jargon.",
                      "valueProps": [
                        "Stops dead stock",
                        "Reorders before the shelf empties"
                      ],
                      "contentAngles": [
                        "The cost of one stockout",
                        "What a good reorder point looks like"
                      ],
                      "keywords": [
                        "inventory forecasting",
                        "reorder point",
                        "dead stock"
                      ],
                      "competitors": [
                        "Inventory Planner"
                      ],
                      "palette": [
                        "#0b3d2e",
                        "#f4f1ea"
                      ],
                      "sourceUrl": "https://northwind.example",
                      "refreshedAt": "2026-08-09T06:00:12Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patchWorkspace",
        "summary": "Correct a workspace’s name, handle, site, logo or timezone.",
        "description": "Every field is optional and a field you leave out is left alone, so renaming a brand does not clear its logo. Send `websiteUrl: null` or `logoUrl: null` to clear one; `name`, `slug` and `timezone` cannot be cleared, because the columns behind them are not nullable. Changing `websiteUrl` does **not** re-read the site. The brand context is derived by `POST /brand/analyze`, and pointing at a new address without asking for a crawl would leave the two disagreeing silently. `timezone` is the one to get right early: it is `UTC` until you set it, and it is what an automation’s posting times are interpreted in. Sending an empty body is `invalid_input` rather than a 200 that changed nothing.\n\nChanging `websiteUrl` leaves the brand context alone. Call POST /brand/analyze afterwards to read the new site.\n\nScope: `workspaces:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "workspaces"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workspace id.",
            "schema": {
              "type": "string",
              "description": "The workspace id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The brand’s display name."
                  },
                  "slug": {
                    "type": "string",
                    "description": "The URL handle, unique within your team. Slugified before it is stored; a handle another workspace holds is `invalid_input`."
                  },
                  "websiteUrl": {
                    "type": "string",
                    "description": "The brand’s site. Send `null` to clear it.",
                    "format": "uri"
                  },
                  "logoUrl": {
                    "type": "string",
                    "description": "The brand’s mark. Send `null` to clear it.",
                    "format": "uri"
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA zone, for example `Europe/Lisbon`. Everything scheduled in this workspace is interpreted in it."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "name": "Northwind Supply",
                "timezone": "Europe/Lisbon"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                    "name": "Northwind Supply",
                    "websiteUrl": "https://northwind.example",
                    "logoUrl": null,
                    "isDefault": true,
                    "archivedAt": null,
                    "brandRefreshedAt": "2026-08-12T09:00:00Z",
                    "createdAt": "2026-07-01T09:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/workspaces/{id}/archive": {
      "post": {
        "operationId": "postWorkspaceArchive",
        "summary": "Archive a workspace.",
        "description": "Stops a brand without losing it. Nothing inside is cancelled: posts keep their slots, automations keep their status, connected accounts stay connected. What changes is that a scheduled post is **withheld** at its slot rather than published, and a launched automation refuses to schedule its next run. Both are checked at the moment the work would run, so a post already going out is not interrupted. The answer carries the two counts that stops, and restoring puts the withheld work back. See `POST /workspaces/{id}/restore` for how each half resumes.\n\nArchiving the team’s default workspace is `409` while any other workspace is still active: choose the new default with POST /workspaces/{id}/default first, because promoting one for you would silently repoint every request that omits `workspaceId`.\n\nArchiving the last active workspace is allowed. The team then has no default, and every call that omits `workspaceId` answers `400 invalid_input` until one is set or named.\n\nA workspace that is already archived is `409`. Archiving frees the plan’s workspace slot.\n\nWhile it is archived, every call that would schedule work or spend credits in it answers `409 conflict`: POST /posts, POST /content/{format}, POST /influencers, POST /influencers/{id}/videos, POST /automations and POST /automations/{id}/launch. Reads, cancellations, DELETE /posts/{id}, DELETE /influencers/{id} and suggestion generation are unaffected; a guard that blocked stopping things would trap you in the state you were leaving.\n\nScope: `workspaces:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "workspaces"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workspace id.",
            "schema": {
              "type": "string",
              "description": "The workspace id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "d0e2f3a4-5b6c-4d7e-8f90-1a2b3c4d5e6f",
                    "name": "Northwind Trade",
                    "isDefault": false,
                    "archivedAt": "2026-08-10T09:12:44Z",
                    "withheldPosts": 12,
                    "withheldAutomations": 1
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/workspaces/{id}/restore": {
      "post": {
        "operationId": "postWorkspaceRestore",
        "summary": "Restore an archived workspace.",
        "description": "Un-archives a brand. Every post that was withheld and whose slot passed while the workspace was archived is put back on the publish queue by this call: a withheld publish is a job that already ran, so clearing the flag alone would leave those posts in the calendar with nothing to publish them, while a post whose slot is still ahead keeps the job it already had. It does **not** become the default again, because the team may have chosen another one in the meantime, so that is a separate call.\n\nA workspace that is not archived is `409`.\n\nRestoring into a full plan is `429 usage_limit_reached`: archiving freed the slot, so restoring needs one back.\n\nAn automation whose launch was refused while the brand was archived is resumed by calling POST /automations/{id}/launch again, which accepts a launched run that scheduled nothing. It is a second call rather than part of this one because a launch carries the sandbox setting of the credential that made it, and nothing records that for a launch that never ran.\n\nScope: `workspaces:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "workspaces"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workspace id.",
            "schema": {
              "type": "string",
              "description": "The workspace id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "d0e2f3a4-5b6c-4d7e-8f90-1a2b3c4d5e6f",
                    "name": "Northwind Trade",
                    "isDefault": false,
                    "archivedAt": null,
                    "withheldPosts": 0,
                    "withheldAutomations": 0
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/workspaces/{id}/default": {
      "post": {
        "operationId": "postWorkspaceDefault",
        "summary": "Set the team’s default workspace.",
        "description": "Points every request that omits `workspaceId` at this workspace, clearing whatever held it. One default per team, and an archived workspace cannot hold it.\n\nA key pinned to one workspace is refused with `403 forbidden_scope`, whatever scopes it carries. The default is team-wide, so honouring it would let a key that can read one brand redirect every other credential’s unqualified traffic into it.\n\nAn archived workspace is `409`. Restore it first.\n\nScope: `workspaces:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "workspaces"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workspace id.",
            "schema": {
              "type": "string",
              "description": "The workspace id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                    "name": "Northwind",
                    "isDefault": true,
                    "archivedAt": null,
                    "withheldPosts": 0,
                    "withheldAutomations": 0
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/workspaces/{id}/brand-kit": {
      "get": {
        "operationId": "getWorkspaceBrandKit",
        "summary": "Read a workspace’s brand kit.",
        "description": "The look every render in this workspace gets: caption colours, font and position, the logo, an outro card, a watermark and the voice. A workspace with no kit answers the defaults, with `null` colours and `updatedAt: null`.\n\nScope: `workspaces:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "workspaces"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workspace id.",
            "schema": {
              "type": "string",
              "description": "The workspace id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "workspaceId": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                    "logoAssetId": "a3c1e2d4-5f60-4718-8a9b-0c1d2e3f4a5b",
                    "primaryColor": "#ff0066",
                    "secondaryColor": "#1a1a1a",
                    "textColor": "#ffffff",
                    "font": "body",
                    "captions": {
                      "preset": "karaoke",
                      "fill": "#ffffff",
                      "stroke": "#000000",
                      "highlight": "#ff0066",
                      "position": "bottom"
                    },
                    "voiceId": "narrator-1",
                    "outro": {
                      "text": "Follow @northwind",
                      "logo": true
                    },
                    "watermark": true,
                    "updatedAt": "2026-08-12T09:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patchWorkspaceBrandKit",
        "summary": "Change a workspace’s brand kit.",
        "description": "A field you leave out keeps its value and `null` clears it. The kit applies to renders that start after the change. `logoAssetId` must name an image asset (PNG) in this workspace. `font` is `display`, `body` or `mono`. `captions.position` is `top`, `middle` or `bottom`. Colours are `#rrggbb`.\n\nA colour that is not `#rrggbb`, an unknown font or a logo from another workspace is `400 invalid_input`.\n\nScope: `workspaces:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "workspaces"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workspace id.",
            "schema": {
              "type": "string",
              "description": "The workspace id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "logoAssetId": {
                    "type": "string",
                    "description": "An image asset in this workspace. Used by the outro and the watermark.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "primaryColor": {
                    "type": "string",
                    "description": "Brand colour, `#rrggbb`. Fills the outro card."
                  },
                  "secondaryColor": {
                    "type": "string",
                    "description": "Second brand colour, `#rrggbb`."
                  },
                  "textColor": {
                    "type": "string",
                    "description": "Outro text colour, `#rrggbb`."
                  },
                  "font": {
                    "type": "string",
                    "description": "`display`, `body` or `mono`."
                  },
                  "captions": {
                    "type": "object",
                    "description": "`preset` (`karaoke` or `block`), `fill`, `stroke`, `highlight` (colours) and `position` (`top`, `middle`, `bottom`)."
                  },
                  "voiceId": {
                    "type": "string",
                    "description": "The voice used when a request names none."
                  },
                  "outro": {
                    "type": "object",
                    "description": "`{ text, logo }`: a 2 second end card. `null` removes it."
                  },
                  "watermark": {
                    "type": "boolean",
                    "description": "Draw the logo (or the workspace name) in the corner."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "primaryColor": "#ff0066",
                "captions": {
                  "highlight": "#ff0066"
                },
                "watermark": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "workspaceId": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                    "logoAssetId": "a3c1e2d4-5f60-4718-8a9b-0c1d2e3f4a5b",
                    "primaryColor": "#ff0066",
                    "secondaryColor": "#1a1a1a",
                    "textColor": "#ffffff",
                    "font": "body",
                    "captions": {
                      "preset": "karaoke",
                      "fill": "#ffffff",
                      "stroke": "#000000",
                      "highlight": "#ff0066",
                      "position": "bottom"
                    },
                    "voiceId": "narrator-1",
                    "outro": {
                      "text": "Follow @northwind",
                      "logo": true
                    },
                    "watermark": true,
                    "updatedAt": "2026-08-12T09:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteWorkspaceBrandKit",
        "summary": "Reset a workspace’s brand kit.",
        "description": "Removes the kit, so later renders use the house look.\n\nScope: `workspaces:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "workspaces"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workspace id.",
            "schema": {
              "type": "string",
              "description": "The workspace id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "workspaceId": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                    "removed": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/accounts": {
      "get": {
        "operationId": "getAccounts",
        "summary": "List connected social accounts.",
        "description": "The ids returned here are the ids `selectedAccounts` takes on a post or an automation. `status` is worth checking before a scheduled run: an `expired` token fails at publish time, and a post scheduled against one will report a per-target failure rather than being rejected up front.\n\nScope: `accounts:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "accounts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Filters to one platform.",
            "schema": {
              "type": "string",
              "enum": [
                "tiktok",
                "instagram",
                "youtube"
              ],
              "description": "Filters to one platform."
            }
          },
          {
            "name": "ownerType",
            "in": "query",
            "required": false,
            "description": "Brand accounts or accounts that belong to an AI influencer.",
            "schema": {
              "type": "string",
              "enum": [
                "brand",
                "influencer"
              ],
              "description": "Brand accounts or accounts that belong to an AI influencer."
            }
          },
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "ff9d429b-07c5-4320-a98c-ac6e1791b828",
                      "platform": "tiktok",
                      "handle": "@northwind",
                      "displayName": "Northwind",
                      "avatarUrl": "https://cdn.looptake.com/ff9d429b-07c5-4320-a98c-ac6e1791b828/avatar.jpg",
                      "ownerType": "brand",
                      "influencerId": null,
                      "scopes": [
                        "video.publish",
                        "user.info.basic"
                      ],
                      "status": "connected",
                      "expiresAt": "2026-10-02T00:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/generations": {
      "post": {
        "operationId": "postGenerations",
        "summary": "Generate suggestions.",
        "description": "Synchronous: the response carries the finished suggestions, each with its on-screen caption, platform body, hashtags, hook score, the angle it came from and, for a Turbo format, the deck that renders it. Generating costs no credits on any plan, which is why this is the one endpoint with a tight rate limit rather than a price.\n\nThe deck is omitted from the list shape and returned in full by GET /generations/{id}.\n\nTen requests per 300 seconds per key. A batch of ten counts as one request.\n\nEvery suggestion carries a brand-safety `review`: `pass`, `flagged`, or `withheld`. A withheld suggestion is returned with the phrase that caused it and cannot be accepted. The batch is never shortened, so `count` suggestions come back whatever the review found.\n\nScope: `generations:write`.\n\nRate limit: 10 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "count": {
                    "type": "integer",
                    "description": "How many suggestions to return.",
                    "default": 3,
                    "minimum": 1,
                    "maximum": 10
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "walloftext",
                      "slideshow",
                      "greenscreen",
                      "talkinghead",
                      "ugc",
                      "characterswap",
                      "listicle",
                      "ranking",
                      "askmeanything",
                      "splitscreen",
                      "grid2x2",
                      "fadein",
                      "hookdemo",
                      "interview",
                      "productspokesperson",
                      "greenscreenmobile",
                      "clonevideo",
                      "statvideo"
                    ],
                    "description": "Pins every suggestion to one format. Left out, the mix is chosen from the brand context."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "A steer for this batch: a launch, a season, an objection to answer. With no brand profile, the prompt is the brief."
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Writes the scripts for one AI influencer, in that influencer’s voice.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which brand to write for. Defaults to the key’s default workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "count": 2,
                "format": "walloftext"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "6522d1ec-2c11-4d3a-b078-6f6cfe9f217b",
                      "workspaceId": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                      "format": "walloftext",
                      "caption": "One stockout costs more than a year of forecasting",
                      "postCaption": "The maths nobody runs before they run out.",
                      "hashtags": [
                        "#retail",
                        "#inventory",
                        "#smallbusiness"
                      ],
                      "why": "The pricing objection, answered with the cost of the alternative.",
                      "angle": "The cost of one stockout",
                      "score": 78,
                      "status": "pending",
                      "review": {
                        "verdict": "pass",
                        "findings": []
                      },
                      "createdAt": "2026-08-12T11:05:02Z"
                    },
                    {
                      "id": "be40861b-3bd3-4b0a-b35f-1aa8835e78eb",
                      "workspaceId": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                      "format": "walloftext",
                      "caption": "Your reorder point is a guess. Here is the number.",
                      "postCaption": "Three inputs, one number, no spreadsheet.",
                      "hashtags": [
                        "#inventory",
                        "#retailtips"
                      ],
                      "why": "Teaches the concept the product automates.",
                      "angle": "What a good reorder point looks like",
                      "score": 71,
                      "status": "pending",
                      "review": {
                        "verdict": "withheld",
                        "findings": [
                          {
                            "rule": "unsupported_claim",
                            "severity": "withhold",
                            "origin": "rule",
                            "quote": "Your reorder point is a guess. Here is the number.",
                            "detail": "`guaranteed` asserts something about the product that nothing in the brand context says"
                          }
                        ]
                      },
                      "createdAt": "2026-08-12T11:05:02Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getGenerations",
        "summary": "List suggestions.",
        "description": "Suggestions owned by an automation are excluded, because an automation holds hundreds and they belong to its review queue rather than to yours. Read those through GET /automations/{id}/posts instead.\n\nScope: `generations:read`.\n\nRate limit: 10 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filters by lifecycle status.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "accepted",
                "scheduled",
                "discarded"
              ],
              "description": "Filters by lifecycle status."
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "One-based page number.",
            "schema": {
              "type": "integer",
              "description": "One-based page number.",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "description": "Items per page.",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "6522d1ec-2c11-4d3a-b078-6f6cfe9f217b",
                      "format": "walloftext",
                      "caption": "One stockout costs more than a year of forecasting",
                      "hashtags": [
                        "#retail",
                        "#inventory"
                      ],
                      "score": 78,
                      "status": "pending",
                      "reviewVerdict": "pass",
                      "automationId": null,
                      "createdAt": "2026-08-12T11:05:02Z"
                    }
                  ],
                  "pagination": {
                    "total": 34,
                    "page": 1,
                    "limit": 1,
                    "pages": 34
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/videos": {
      "post": {
        "operationId": "postVideos",
        "summary": "Make a video from one sentence.",
        "description": "No brand profile needed. The agent writes a brief, scores hook candidates, writes the script, picks the format, adds an AI still and voice, times the captions and renders 9:16. Returns at once; poll GET /videos/{id}. Text-only videos are free. A still costs 3 credits and voice 1 credit per 15 seconds; unused credits come back when the video is done, and all of them on failure.\n\nStudio formats such as talking heads use POST /content/{kind}.\n\nIf the team cannot cover the still and no voice is asked for, the video is made without the still and costs nothing.\n\nScope: `generations:write`.\n\nRate limit: 10 requests per 300 seconds.\n\nCosts 3 credits per AI still.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "prompt": {
                    "type": "string",
                    "description": "The video, in one sentence. Up to 600 characters.",
                    "maximum": 600
                  },
                  "length": {
                    "type": "string",
                    "enum": [
                      "15s",
                      "30s",
                      "60s"
                    ],
                    "description": "Target length.",
                    "default": "30s"
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "walloftext",
                      "slideshow",
                      "greenscreen",
                      "listicle",
                      "ranking",
                      "splitscreen",
                      "askmeanything",
                      "statvideo",
                      "fadein"
                    ],
                    "description": "Left out or `auto`, the agent picks."
                  },
                  "language": {
                    "type": "string",
                    "description": "Language of the script, voice and captions.",
                    "default": "en"
                  },
                  "voice": {
                    "type": "string",
                    "description": "A voice id such as `nova`. Left out, no voice-over."
                  },
                  "count": {
                    "type": "integer",
                    "description": "How many videos to make from the best hooks.",
                    "default": 1,
                    "minimum": 1,
                    "maximum": 4
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which workspace. Defaults to the key’s default workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "required": [
                  "prompt"
                ],
                "additionalProperties": false
              },
              "example": {
                "prompt": "Three mistakes new runners make, and the fix for each",
                "length": "30s"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "3f2b8f4e-6a51-4c55-9d0b-5c0f1b7e2a10",
                    "workspaceId": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                    "prompt": "Three mistakes new runners make, and the fix for each",
                    "length": "30s",
                    "format": null,
                    "status": "queued",
                    "stage": "hook",
                    "stages": [
                      "hook",
                      "script",
                      "visuals",
                      "voice",
                      "captions",
                      "music",
                      "render"
                    ],
                    "progress": 0,
                    "viralScore": null,
                    "hooks": [],
                    "outputs": [],
                    "creditsReserved": 3,
                    "creditsSpent": 0,
                    "statusUrl": "https://looptake.com/api/v1/videos/3f2b8f4e-6a51-4c55-9d0b-5c0f1b7e2a10"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/remix": {
      "post": {
        "operationId": "postRemix",
        "summary": "Remix a viral video onto your topic.",
        "description": "Give a TikTok, YouTube or Instagram link. We read its public title, caption and author (oEmbed, or the page tags), the agent extracts the structure (hook pattern, beats, pacing, format, on-screen text) and writes a new video on your topic with the same shape, through the same pipeline as POST /videos. The source video is never downloaded or reused. With no `topic`, the workspace brand is the topic. Poll GET /videos/{id}; `source.structure` shows what was copied. Costs as POST /videos.\n\nScope: `generations:write`.\n\nRate limit: 10 requests per 300 seconds.\n\nCosts 3 credits per AI still.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "The video to learn from: a TikTok, YouTube or Instagram link, or any page with og tags.",
                    "format": "uri"
                  },
                  "topic": {
                    "type": "string",
                    "description": "What your video is about. Left out, the workspace brand is used; a workspace with no brand needs a topic.",
                    "maximum": 600
                  },
                  "length": {
                    "type": "string",
                    "enum": [
                      "15s",
                      "30s",
                      "60s"
                    ],
                    "description": "Target length.",
                    "default": "30s"
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "walloftext",
                      "slideshow",
                      "greenscreen",
                      "listicle",
                      "ranking",
                      "splitscreen",
                      "askmeanything",
                      "statvideo",
                      "fadein"
                    ],
                    "description": "Left out or `auto`, the source structure picks."
                  },
                  "language": {
                    "type": "string",
                    "description": "Language of the script, voice and captions.",
                    "default": "en"
                  },
                  "voice": {
                    "type": "string",
                    "description": "A voice id such as `nova`. Left out, no voice-over."
                  },
                  "count": {
                    "type": "integer",
                    "description": "How many videos to make.",
                    "default": 1,
                    "minimum": 1,
                    "maximum": 4
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which workspace. Defaults to the key’s default workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "required": [
                  "url"
                ],
                "additionalProperties": false
              },
              "example": {
                "url": "https://www.tiktok.com/@runcoach/video/7412345678901234567",
                "topic": "Meal prep for night-shift nurses"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "5c1d7a0e-1f2b-4c3d-8e9f-0a1b2c3d4e5f",
                    "kind": "remix",
                    "prompt": "Meal prep for night-shift nurses",
                    "source": {
                      "url": "https://www.tiktok.com/@runcoach/video/7412345678901234567",
                      "platform": "tiktok",
                      "topic": "Meal prep for night-shift nurses"
                    },
                    "status": "queued",
                    "stage": "hook",
                    "progress": 0,
                    "creditsReserved": 3,
                    "statusUrl": "https://looptake.com/api/v1/videos/5c1d7a0e-1f2b-4c3d-8e9f-0a1b2c3d4e5f"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/clips": {
      "post": {
        "operationId": "postClips",
        "summary": "Cut a long video into shorts.",
        "description": "Send a video asset or a public media URL. The worker fetches it, transcribes it with timestamps, the agent picks the best self-contained moments (strong hook, clear payoff, 15 to 60 seconds), and each one is cut, reframed to 9:16 (centre crop over a blurred fill) and given word-timed captions in your brand kit style. Each clip is a content item with a viral score. Costs 1 credit per started minute of source: the known length is reserved, or up to 60 minutes when it is not known, and the rest comes back. Poll GET /videos/{id}.\n\nEach output carries `startMs`, `endMs`, `title`, `hook`, `viralScore`, `contentId` and `previewUrl`.\n\nA source longer than the reservation fails and every credit comes back.\n\nScope: `generations:write`.\n\nRate limit: 10 requests per 300 seconds.\n\nCosts 1 credits per minute of source video.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "assetId": {
                    "type": "string",
                    "description": "A video in the asset library. Send this or `mediaUrl`.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "mediaUrl": {
                    "type": "string",
                    "description": "A public URL of the video file, up to 200 MB. Send this or `assetId`.",
                    "format": "uri"
                  },
                  "count": {
                    "type": "integer",
                    "description": "How many clips to cut.",
                    "default": 3,
                    "minimum": 1,
                    "maximum": 10
                  },
                  "length": {
                    "type": "string",
                    "enum": [
                      "15s",
                      "30s",
                      "60s"
                    ],
                    "description": "The longest a clip may run. Clips are at least 15 seconds.",
                    "default": "60s"
                  },
                  "language": {
                    "type": "string",
                    "description": "Language of the speech, titles and hooks.",
                    "default": "en"
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which workspace. Defaults to the key’s default workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "mediaUrl": "https://cdn.example.com/podcast-ep-12.mp4",
                "count": 5
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
                    "kind": "clip",
                    "prompt": "Clips from podcast-ep-12.mp4",
                    "count": 5,
                    "status": "queued",
                    "stage": "ingest",
                    "stages": [
                      "ingest",
                      "transcribe",
                      "select",
                      "cut"
                    ],
                    "progress": 0,
                    "creditsReserved": 60,
                    "statusUrl": "https://looptake.com/api/v1/videos/9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/videos/bulk": {
      "post": {
        "operationId": "postVideosBulk",
        "summary": "Make up to 100 videos from a CSV.",
        "description": "Send JSON `{ csv }` or `{ rows }`, a `text/csv` body, or a multipart form with a `file` field. Columns: `prompt` (required), `length`, `format`, `schedule_at`, `accounts`, `voice`, `language`, `made_for_kids`. Every row is checked first; one bad row creates nothing and the 400 names every bad row and line. A row with `schedule_at` and `accounts` (account ids or platform names) is scheduled to post when its video is ready; YouTube needs `made_for_kids`. Poll GET /videos/bulk/{id}. Each row costs as POST /videos.\n\nScope: `generations:write`.\n\nRate limit: 10 requests per 300 seconds.\n\nCosts 3 credits per AI still.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "csv": {
                    "type": "string",
                    "description": "The CSV text, with a header row. Send this or `rows`."
                  },
                  "rows": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Rows as objects with the same keys as the CSV columns. Send this or `csv`."
                  },
                  "timezone": {
                    "type": "string",
                    "description": "The zone a `schedule_at` without an offset is read in. IANA name, for example \"Europe/Berlin\".",
                    "default": "UTC"
                  },
                  "validateOnly": {
                    "type": "boolean",
                    "description": "Check the rows and create nothing.",
                    "default": false
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which workspace. Defaults to the key’s default workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "csv": "prompt,length,schedule_at,accounts\n\"Three tips for new runners\",30s,2026-11-02T09:00:00Z,tiktok\n"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "batchId": "2b4c6d8e-0f1a-4b3c-9d5e-7f8a9b0c1d2e",
                    "total": 1,
                    "queued": 1,
                    "running": 0,
                    "ready": 0,
                    "failed": 0,
                    "progress": 0,
                    "done": false,
                    "statusUrl": "https://looptake.com/api/v1/videos/bulk/2b4c6d8e-0f1a-4b3c-9d5e-7f8a9b0c1d2e",
                    "videos": [
                      {
                        "id": "6e5d4c3b-2a19-4807-b6f5-e4d3c2b1a090",
                        "prompt": "Three tips for new runners",
                        "status": "queued",
                        "stage": "hook",
                        "progress": 0,
                        "scheduleAt": "2026-11-02T09:00:00Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/videos/bulk/{id}": {
      "get": {
        "operationId": "getVideosBulk",
        "summary": "Read a bulk batch’s progress.",
        "description": "Counts by status, the average `progress`, `done` once every row is `ready` or `failed`, and each row’s status and outputs.\n\nScope: `generations:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The batch id from POST /videos/bulk.",
            "schema": {
              "type": "string",
              "description": "The batch id from POST /videos/bulk.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "batchId": "2b4c6d8e-0f1a-4b3c-9d5e-7f8a9b0c1d2e",
                    "total": 2,
                    "queued": 0,
                    "running": 1,
                    "ready": 1,
                    "failed": 0,
                    "progress": 70,
                    "done": false,
                    "videos": []
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/generations/{id}": {
      "get": {
        "operationId": "getGeneration",
        "summary": "Read one suggestion in full.",
        "description": "Adds the three things the list shape omits: the rationale, the deck, and the brand-safety review in full. The deck is the render blueprint: normalised geometry, slides, text boxes, transitions and captions. It is editable, and an edited deck sent to POST /posts renders exactly what you sent. The review carries one finding per problem, each with the phrase it is about; the list shape carries only `reviewVerdict`.\n\nScope: `generations:read`.\n\nRate limit: 10 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The suggestion id.",
            "schema": {
              "type": "string",
              "description": "The suggestion id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "6522d1ec-2c11-4d3a-b078-6f6cfe9f217b",
                    "format": "walloftext",
                    "caption": "One stockout costs more than a year of forecasting",
                    "postCaption": "The maths nobody runs before they run out.",
                    "hashtags": [
                      "#retail",
                      "#inventory",
                      "#smallbusiness"
                    ],
                    "why": "The pricing objection, answered with the cost of the alternative.",
                    "angle": "The cost of one stockout",
                    "score": 78,
                    "status": "pending",
                    "review": {
                      "verdict": "pass",
                      "findings": []
                    },
                    "deck": {
                      "version": 1,
                      "ratio": "9:16",
                      "durationMs": 8000,
                      "fps": 30,
                      "background": {
                        "kind": "clip",
                        "src": "a7273471-d932-4f9a-958e-79abdd1f3fd7"
                      },
                      "slides": [
                        {
                          "startMs": 0,
                          "endMs": 8000,
                          "boxes": [
                            {
                              "id": "hook",
                              "text": "One stockout costs more than a year of forecasting",
                              "rect": {
                                "x": 0.08,
                                "y": 0.18,
                                "w": 0.84,
                                "h": 0.34
                              }
                            }
                          ]
                        }
                      ],
                      "captions": {
                        "enabled": true,
                        "style": "karaoke",
                        "position": 0.78
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/generations/{id}/accept": {
      "post": {
        "operationId": "postGenerationAccept",
        "summary": "Accept a suggestion into content.",
        "description": "Turns a suggestion into a renderable content piece and returns its `contentId`. Accepting counts against the plan’s saved-post quota; generating did not. A Turbo format is renderable immediately, a Studio format begins its render, and either way the id is what POST /posts takes as `contentId`.\n\nAccepting a suggestion twice returns the same content id rather than a second piece.\n\nA suggestion whose brand-safety review withheld it is 409 `conflict`, and no quota is consumed. POST /posts with a `suggestionId` refuses it the same way. Read GET /generations/{id} for the finding, then edit the copy or generate another.\n\nScope: `generations:write`.\n\nRate limit: 10 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The suggestion id.",
            "schema": {
              "type": "string",
              "description": "The suggestion id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "c02119cb-ed7a-40ed-87fe-c18edc1fa1a2",
                    "generationId": "6522d1ec-2c11-4d3a-b078-6f6cfe9f217b",
                    "format": "walloftext",
                    "renderStatus": "pending",
                    "creditsSpent": 0
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/generations/{id}/variants": {
      "post": {
        "operationId": "postGenerationVariants",
        "summary": "Create A/B hook variants.",
        "description": "Rewrites the hook and keeps everything else, so the only difference between the variants is the line that has to stop the scroll. Angle, deck, format and hashtags are carried over. Each variant is a suggestion in its own right, with its own id and its own hook score.\n\nScope: `generations:write`.\n\nRate limit: 10 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "generations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The suggestion to vary.",
            "schema": {
              "type": "string",
              "description": "The suggestion to vary.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "count": {
                    "type": "integer",
                    "description": "How many variants to write.",
                    "default": 3,
                    "minimum": 2,
                    "maximum": 5
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "count": 2
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "ba2bfff0-0078-45f8-8352-0967f185cba5",
                      "remixSource": "6522d1ec-2c11-4d3a-b078-6f6cfe9f217b",
                      "caption": "The stockout you did not budget for",
                      "score": 74,
                      "status": "pending",
                      "reviewVerdict": "pass"
                    },
                    {
                      "id": "e70157dd-5ee2-4667-a594-61f9c20ebaf7",
                      "remixSource": "6522d1ec-2c11-4d3a-b078-6f6cfe9f217b",
                      "caption": "Empty shelf, full warehouse. Both cost the same.",
                      "score": 81,
                      "status": "pending"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/posts": {
      "post": {
        "operationId": "postPosts",
        "summary": "Create and schedule a post.",
        "description": "Exactly one content source, resolved in a fixed order: `suggestionId`, then `contentId`, then `deck` with `format`, then `videoUrl` or `images`. Sending two sources is not an error; the first one in that order wins, which keeps the resolution deterministic. A post that has to produce its video before it can publish returns 202 and a `statusUrl`: a deck has to be rendered, and a `videoUrl` or `images` post has to be fetched into our own storage first.\n\n`approvalStatus` is on this response because it is the only place the common case sees it: a scheduled post with nothing to render is 201 and terminal, so nothing polls `statusUrl` afterwards. `pending` means a person still has to release it and the scheduled time passing does not publish it or cancel it. `null` means no approval was demanded of this post, which is not the same as approved and is not a block: read a missing value as neither.\n\nA post built from a `deck`, a `videoUrl` or `images` returns 202 rather than 201. Poll `statusUrl` until the status is terminal.\n\nThe media behind `videoUrl` and `images` is fetched once, when the post is created, and published from our copy. A URL that redirects to a private address, answers with something that is not a video or an image, or is larger than 200 MB is refused, and `renderStatus` becomes `failed` with the reason on the post.\n\nA video is measured after it is fetched and checked against each selected platform: a video longer than YouTube Shorts accepts, or in an aspect ratio Instagram will not publish, fails the post before anything reaches a platform.\n\nA post that names a YouTube account and does not send `declarations.madeForKids` is refused here, before anything is rendered or charged. YouTube requires that answer on every upload and it is the account holder’s to give: we send what you state, and nothing when you state nothing.\n\nA field this endpoint does not have is a 400, not a field we ignore. A misspelled key and a capability this build does not carry are both refused at the door, so `altText` and `hashtagPlacement` are refusals rather than fields we store and never send. `firstComment` was on that list until 2026-08-19 and is not any more: it is a real field now, inside `perPlatform`, on the two networks whose API can post a comment. At the TOP level it is still a 400, and deliberately so — a first comment is per network because one of the three cannot post one at all. The same rule already applied inside `perPlatform`; since 2026-08-19 it applies to the whole body.\n\nHonours `Idempotency-Key`. The same key with the same body replays the stored response; the same key with a different body is a 409.\n\nSixty requests per 300 seconds per key.\n\nScope: `posts:write`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "suggestionId": {
                    "type": "string",
                    "description": "Source 1. Accepts the suggestion and posts it in one call.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "contentId": {
                    "type": "string",
                    "description": "Source 2. An already-accepted content piece.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "deck": {
                    "type": "object",
                    "description": "Source 3. A render blueprint. Requires `format`, and returns 202."
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "walloftext",
                      "slideshow",
                      "greenscreen",
                      "talkinghead",
                      "ugc",
                      "characterswap",
                      "listicle",
                      "ranking",
                      "askmeanything",
                      "splitscreen",
                      "grid2x2",
                      "fadein",
                      "hookdemo",
                      "interview",
                      "productspokesperson",
                      "greenscreenmobile",
                      "clonevideo",
                      "statvideo"
                    ],
                    "description": "Required with `deck`, ignored otherwise."
                  },
                  "videoUrl": {
                    "type": "string",
                    "description": "Source 4. Media you already have. Fetched into our own storage before the post publishes, so the post does not depend on your URL still answering at its scheduled time. Returns 202.",
                    "format": "uri"
                  },
                  "images": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Source 4. Image URLs, fetched into our own storage the same way as `videoUrl`. Every entry is stored, and only the FIRST becomes the post media, which is the one asset every selected platform receives. There is no photo carousel in this build: all three clients publish through their video endpoints, so an image post sends a single still on a video call and the entries after the first reach no platform at all. `POST /posts/preview` names them in `notSent`. Returns 202."
                  },
                  "thumbnailUrl": {
                    "type": "string",
                    "description": "Cover frame. Only read alongside `videoUrl`.",
                    "format": "uri"
                  },
                  "selectedAccounts": {
                    "type": "object",
                    "description": "Platform to account ids, for example {\"tiktok\":[\"ff9d429b-07c5-4320-a98c-ac6e1791b828\"]}. At least one list must be non-empty."
                  },
                  "schedule": {
                    "type": "string",
                    "enum": [
                      "asap",
                      "scheduled",
                      "draft"
                    ],
                    "description": "When this post goes out, and whether it goes out at all yet. `asap` publishes as soon as the render finishes and the post reads `queued`. `scheduled` publishes at `scheduledTime`. `draft` saves it unfinished: the post reads `draft`, no publisher is queued for it, and **nothing sends it until you call POST /posts/{id}/publish**. A draft may carry a `scheduledTime` as well, and releasing it then sends it at that instant rather than at once, which is what lets you pencil in a slot for a post you have not finished writing.",
                    "default": "asap"
                  },
                  "scheduledTime": {
                    "type": "string",
                    "description": "ISO 8601, in the future. Required when `schedule` is `scheduled`, and optional when it is `draft`.",
                    "format": "date-time"
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA zone the schedule is read in, for example Europe/Lisbon. IANA name, for example \"Europe/Berlin\".",
                    "default": "UTC"
                  },
                  "caption": {
                    "type": "string",
                    "description": "Overrides the platform body. Validated before the post is accepted against the limit of each platform that receives it, which is every selected platform unless `perPlatform` gives one its own words."
                  },
                  "hashtags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Appended to the caption, per platform convention."
                  },
                  "perPlatform": {
                    "type": "object",
                    "description": "The words one platform gets instead of `caption` and `hashtags`, keyed by platform: `{\"tiktok\":{\"caption\":\"…\",\"hashtags\":[\"…\"]},\"youtube\":{\"title\":\"…\"}}`. Every field inside is optional and leaving one out means that platform inherits the post’s. A platform you do not name here publishes exactly what it always did, so a body without this field behaves identically to one written before it existed. `title` is only accepted for a platform that has a title field, which is YouTube; a title for TikTok or Instagram is a 400 rather than a value we store and never send. Naming a platform this post does not publish to is a 400 for the same reason. This is what makes a caption legal that one platform would refuse: the limit is checked against the text each account actually receives, so a 3,000-unit YouTube caption with a shorter TikTok one beside it is accepted where the single caption was not. It is keyed by platform and not by account: two TikTok accounts in one workspace get one TikTok caption. `firstComment` is the fourth field inside a block: the comment we post underneath the moment that network has the post, which is where hashtags go if you want the caption to read as a sentence. It is accepted for Instagram and YouTube and is a 400 for TikTok naming the platform, because TikTok publishes no comment-write endpoint in any public API — not a limit of this build, and no release of it will change the answer. It is also a 400 on a target set to `draft` or `inbox`, because a comment goes under a live post and neither of those produces one. Read `firstComment` on each target of `GET /posts/{id}` to see whether it landed: it is a SECOND call to the network, made after the post is live, so it can fail on its own, and when it does the post stays `posted` and that field carries the reason."
                  },
                  "ownerType": {
                    "type": "string",
                    "enum": [
                      "brand",
                      "influencer"
                    ],
                    "description": "Whose accounts this publishes from.",
                    "default": "brand"
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Required when `ownerType` is `influencer`.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "publishMode": {
                    "type": "string",
                    "enum": [
                      "publish",
                      "draft",
                      "inbox"
                    ],
                    "description": "How far to take the publish. `publish` goes live. `draft` uploads the video and leaves it unpublished for you to finish, which only YouTube can do: it becomes a private upload in YouTube Studio. `inbox` delivers the video to the creator’s app as a notification, which only TikTok can do. One value applies to the whole post, so a post that names a YouTube and a TikTok account and asks for `draft` is a 400 naming TikTok, not a post that holds one back and publishes the other. Set `tiktokSendToInbox` alongside `draft` to hold the YouTube upload back and route the TikTok copy to the inbox, which is the closest TikTok gets to not publishing.",
                    "default": "publish"
                  },
                  "tiktokSendToInbox": {
                    "type": "boolean",
                    "description": "The TikTok-scoped spelling of `publishMode: \"inbox\"`, and it applies only to this post’s TikTok target. The video arrives in your TikTok notifications rather than the Drafts folder, and it carries no caption: TikTok’s inbox endpoint has no caption field, so the generated caption and hashtags stay on this post and you paste them in before publishing. This is not the same thing as `publishMode: \"draft\"`, which is an unpublished upload and which TikTok cannot do at all.",
                    "default": false
                  },
                  "tiktokOptions": {
                    "type": "object",
                    "description": "TikTok publishing options for this post. `disableComment`, `disableDuet` and `disableStitch`, or the permissive spellings `allowComments`, `allowDuet` and `allowStitch`. Anything you do not name is left at the account default."
                  },
                  "declarations": {
                    "type": "object",
                    "description": "Statements the platforms require from the account holder, not publishing options. `madeForKids` is YouTube’s audience declaration and is required whenever this post names a YouTube account: we will not answer it on your behalf, so a post that targets YouTube without it is a 400. `brandedContent` discloses a paid partnership to TikTok and `brandOrganic` discloses that you are promoting your own business; leave either out and nothing is sent, which is where TikTok’s own composer starts. `aiGenerated` is TikTok’s `is_aigc` and `containsSyntheticMedia` is YouTube’s `status.containsSyntheticMedia`: two fields rather than one, because the two platforms word the question differently and we never send one platform the answer you gave the other. Neither is required by its platform and neither is required here, so a post that states nothing about how its video was made publishes with the key absent. Almost everything scheduled from the Looptake composer is generated, so the composer and the automation planner will not submit without an answer, on the paths that carry one. A TikTok post routed to the creator’s inbox is asked nothing, because that endpoint takes the video and no fields at all, and an Instagram-only post is asked nothing because Instagram carries no declaration field. Neither refusal exists at this API, which accepts any of these stated or unstated, and Instagram receives none of these on any path. An unrecognised key in this object is a 400 rather than a field we ignore."
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which brand this post belongs to.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "required": [
                  "selectedAccounts"
                ],
                "additionalProperties": false
              },
              "example": {
                "suggestionId": "6522d1ec-2c11-4d3a-b078-6f6cfe9f217b",
                "selectedAccounts": {
                  "tiktok": [
                    "ff9d429b-07c5-4320-a98c-ac6e1791b828"
                  ],
                  "youtube": [
                    "1c74c860-000b-4136-88a7-a17f31019346"
                  ]
                },
                "schedule": "scheduled",
                "scheduledTime": "2026-09-01T15:00:00Z",
                "timezone": "Europe/Lisbon",
                "declarations": {
                  "madeForKids": false,
                  "brandedContent": true,
                  "aiGenerated": true,
                  "containsSyntheticMedia": true
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                    "status": "scheduled",
                    "renderStatus": "pending",
                    "scheduledTime": "2026-09-01T15:00:00Z",
                    "timezone": "Europe/Lisbon",
                    "approvalStatus": "pending",
                    "statusUrl": "https://looptake.com/api/v1/posts/973821cf-5cc7-44f7-8795-e68da31c59ef",
                    "targets": [
                      {
                        "accountId": "ff9d429b-07c5-4320-a98c-ac6e1791b828",
                        "platform": "tiktok",
                        "status": "scheduled"
                      },
                      {
                        "accountId": "1c74c860-000b-4136-88a7-a17f31019346",
                        "platform": "youtube",
                        "status": "scheduled"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getPosts",
        "summary": "List posts.",
        "description": "Everything scheduled, publishing, published or failed, newest scheduled time first. `from` and `to` filter on the scheduled time rather than the creation time, which is what a calendar sync wants. Every row names the presenter it was published as, or null when it was published for the brand.\n\n`approvalStatus` carries the verdict alone on this shape; `GET /posts/{id}` carries the whole record, including who decided and why. `null` means no approval was demanded of this post and it publishes at its time like any other.\n\nAn `approval` value that is not one of the three is 400 `invalid_input` rather than an ignored parameter. A filter that quietly returned everything would answer a review queue with a whole workspace, and the page would look like it worked.\n\nScope: `posts:read`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filters by post status. `draft` is a post saved unfinished that nothing will publish; `queued` is one going out as soon as its render finishes. Until 2026-08-19 there was one value called `draft` and it meant the second of those.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "queued",
                "scheduled",
                "publishing",
                "posted",
                "partial",
                "failed",
                "cancelled"
              ],
              "description": "Filters by post status. `draft` is a post saved unfinished that nothing will publish; `queued` is one going out as soon as its render finishes. Until 2026-08-19 there was one value called `draft` and it meant the second of those."
            }
          },
          {
            "name": "approval",
            "in": "query",
            "required": false,
            "description": "The review queue, as a filter. `pending` is what is waiting on a person, `rejected` is what was refused and can be re-opened, and `none` is posts nobody was asked about, which is not the same as approved-yet-unpublished and does not include the pending ones. A separate axis from `status`: a post is `scheduled` and `pending` at the same time. There is no `approved` value, because approved posts are already reachable by every other filter here and a queue is what is still open.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "rejected",
                "none"
              ],
              "description": "The review queue, as a filter. `pending` is what is waiting on a person, `rejected` is what was refused and can be re-opened, and `none` is posts nobody was asked about, which is not the same as approved-yet-unpublished and does not include the pending ones. A separate axis from `status`: a post is `scheduled` and `pending` at the same time. There is no `approved` value, because approved posts are already reachable by every other filter here and a queue is what is still open."
            }
          },
          {
            "name": "automationId",
            "in": "query",
            "required": false,
            "description": "Only posts produced by one automation.",
            "schema": {
              "type": "string",
              "description": "Only posts produced by one automation.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          },
          {
            "name": "influencerId",
            "in": "query",
            "required": false,
            "description": "Only posts published as one presenter. This is how you list what went out under a likeness whose subject has withdrawn consent: a withdrawal stops the next render and leaves live posts alone, and this filter is what tells you which posts those are. Delete the presenter and the filter answers nothing for it, because deleting a presenter clears the link on every post it published, so take the list first.",
            "schema": {
              "type": "string",
              "description": "Only posts published as one presenter. This is how you list what went out under a likeness whose subject has withdrawn consent: a withdrawal stops the next render and leaves live posts alone, and this filter is what tells you which posts those are. Delete the presenter and the filter answers nothing for it, because deleting a presenter clears the link on every post it published, so take the list first.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound on the scheduled time.",
            "schema": {
              "type": "string",
              "description": "Inclusive lower bound on the scheduled time.",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Exclusive upper bound on the scheduled time.",
            "schema": {
              "type": "string",
              "description": "Exclusive upper bound on the scheduled time.",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "One-based page number.",
            "schema": {
              "type": "integer",
              "description": "One-based page number.",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "description": "Items per page.",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                      "contentId": "c02119cb-ed7a-40ed-87fe-c18edc1fa1a2",
                      "status": "scheduled",
                      "renderStatus": "ready",
                      "schedule": "scheduled",
                      "scheduledTime": "2026-09-01T15:00:00Z",
                      "timezone": "Europe/Lisbon",
                      "ownerType": "brand",
                      "influencerId": null,
                      "automationId": null,
                      "approvalStatus": "pending",
                      "targets": [
                        {
                          "platform": "tiktok",
                          "accountId": "ff9d429b-07c5-4320-a98c-ac6e1791b828",
                          "status": "scheduled"
                        }
                      ]
                    }
                  ],
                  "pagination": {
                    "total": 18,
                    "page": 1,
                    "limit": 1,
                    "pages": 18
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/posts/preview": {
      "post": {
        "operationId": "postPostsPreview",
        "summary": "See what each platform would receive, before the post exists.",
        "description": "Takes the body of `POST /posts` and answers with the publish request each selected account would be sent: the exact caption-and-hashtag text, the derived YouTube title, the visibility, the TikTok switches and the declarations, built by the same function the publisher builds it with. Nothing is written, nothing is queued and nothing is charged. The two refusals that are per platform arrive per target rather than as a status: a caption too long for one of your accounts and an unstated `declarations.madeForKids` are `refusal` on that target and `200` overall, so you see every account’s answer at once instead of the first one. Everything else refuses exactly as `POST /posts` does.\n\n`bodyText` is the string the platform receives, caption and hashtags joined. It is `null` where the platform receives no text at all, which today is a TikTok target in `inbox` mode: that endpoint takes the video and nothing else, and the creator writes the caption in the app.\n\n`title` is set only for a platform that has a title field, which is YouTube. Write one with `perPlatform.youtube.title` and it is what you see here and what is uploaded; write nothing and it is the first line of the caption cut to YouTube’s 100 units, which is what this product did before a title could be written. Either way this is where you see which line that is.\n\n`firstComment` is the text that network receives as a comment under the post, or `null` where it receives none. It is the one field here that is not part of the publish request: no network carries a comment on the call that creates the post, because a comment needs the id that call returns, so this is always a second call made after the post is live. `null` on a TikTok target is the whole reason to preview it — TikTok has no comment endpoint, `POST /posts` refuses a first comment for it outright, and this is where you see that before you schedule.\n\n`privacy` is `public` on every target. Nothing on the request, the post row or the job payload can set it in this build, so the preview reports the value rather than offering a choice.\n\n`notSent` lists request fields this build accepts and sends to no platform. `thumbnailUrl` is one: it is fetched, stored and shown in the dashboard, and no platform receives it, so each of them picks its own frame. `images[1:]` is the other: every image you send is fetched and stored, and only the first becomes the post media, because the publish plan carries one asset and no client here has a photo-carousel call.\n\n`measured` is false unless you send a `contentId` whose render has finished. A video that has not been fetched or rendered yet has no length and no shape, so the two checks that read them have not run. They run at ingest, and again in the publisher.\n\n`refusal` carries the platform’s own sentence, produced by the publisher’s own pre-flight. A target with a refusal will not publish; the rest of the post still will.\n\nA field this endpoint does not have is a 400, not a field we ignore. A misspelled key and a capability this build does not carry are both refused at the door, so `altText` and `hashtagPlacement` are refusals rather than fields we store and never send. `firstComment` was on that list until 2026-08-19 and is not any more: it is a real field now, inside `perPlatform`, on the two networks whose API can post a comment. At the TOP level it is still a 400, and deliberately so — a first comment is per network because one of the three cannot post one at all. The same rule already applied inside `perPlatform`; since 2026-08-19 it applies to the whole body.\n\nSixty requests per 300 seconds per key, shared with the rest of `/posts`.\n\nScope: `posts:write`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "selectedAccounts": {
                    "type": "object",
                    "description": "Platform to account ids, exactly as on `POST /posts`. One preview target comes back per account."
                  },
                  "contentId": {
                    "type": "string",
                    "description": "A piece that has already rendered. Its measured length and aspect ratio are checked against each platform, which is the only way this endpoint can answer for the video. See `measured` in the response.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "caption": {
                    "type": "string",
                    "description": "The caption body. Previewed exactly as it would be stored: trimmed, and empty when you send none, because nothing fills it in from the source."
                  },
                  "hashtags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Appended to the caption in `bodyText`, each normalised to a single leading #."
                  },
                  "perPlatform": {
                    "type": "object",
                    "description": "Exactly as on `POST /posts`. Worth previewing above everything else here, because it is the one field whose whole purpose is that two of your accounts receive different things: each target comes back with its own `bodyText` and its own `title`, built by the same function the publisher builds them with."
                  },
                  "publishMode": {
                    "type": "string",
                    "enum": [
                      "publish",
                      "draft",
                      "inbox"
                    ],
                    "description": "How far to take the publish. A mode a named platform cannot honour is a 400 here, exactly as on `POST /posts`.",
                    "default": "publish"
                  },
                  "tiktokSendToInbox": {
                    "type": "boolean",
                    "description": "Route the TikTok target to the creator’s inbox. Worth previewing: the inbox endpoint carries no caption, no switches and no disclosures at all, so that target comes back with a null `bodyText` and an empty `declarations`. That includes the AI one, because `is_aigc` rides on the `post_info` block the inbox endpoint has no room for.",
                    "default": false
                  },
                  "tiktokOptions": {
                    "type": "object",
                    "description": "The same object `POST /posts` takes, including the permissive spellings. What comes back is the value the publisher would send, not the value you wrote."
                  },
                  "declarations": {
                    "type": "object",
                    "description": "The same object `POST /posts` takes. Each target lists only the declarations its own platform carries, with the value you stated or null where you stated nothing. This is where you see that `aiGenerated` reached TikTok and `containsSyntheticMedia` reached YouTube, and that neither reached the other."
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which brand the accounts belong to.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "required": [
                  "selectedAccounts"
                ],
                "additionalProperties": false
              },
              "example": {
                "selectedAccounts": {
                  "tiktok": [
                    "ff9d429b-07c5-4320-a98c-ac6e1791b828"
                  ],
                  "youtube": [
                    "1c74c860-000b-4136-88a7-a17f31019346"
                  ]
                },
                "caption": "One stockout costs more than a year of forecasting",
                "hashtags": [
                  "retail",
                  "ops"
                ],
                "perPlatform": {
                  "tiktok": {
                    "caption": "Your stockroom is lying to you",
                    "hashtags": [
                      "fyp"
                    ]
                  }
                },
                "declarations": {
                  "brandedContent": true,
                  "aiGenerated": true,
                  "containsSyntheticMedia": false
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "measured": false,
                    "durationSeconds": null,
                    "ratio": null,
                    "notSent": [
                      "thumbnailUrl",
                      "images[1:]"
                    ],
                    "targets": [
                      {
                        "accountId": "ff9d429b-07c5-4320-a98c-ac6e1791b828",
                        "platform": "tiktok",
                        "publishMode": "publish",
                        "uploadSource": "url",
                        "bodyText": "Your stockroom is lying to you #fyp",
                        "title": null,
                        "privacy": "public",
                        "interaction": {
                          "disableComment": false,
                          "disableDuet": false,
                          "disableStitch": false
                        },
                        "firstComment": null,
                        "declarations": [
                          {
                            "field": "declarations.brandedContent",
                            "stated": true
                          },
                          {
                            "field": "declarations.brandOrganic",
                            "stated": null
                          },
                          {
                            "field": "declarations.aiGenerated",
                            "stated": true
                          }
                        ],
                        "refusal": null
                      },
                      {
                        "accountId": "1c74c860-000b-4136-88a7-a17f31019346",
                        "platform": "youtube",
                        "publishMode": "publish",
                        "uploadSource": "bytes",
                        "bodyText": "One stockout costs more than a year of forecasting #retail #ops",
                        "title": "One stockout costs more than a year of forecasting",
                        "privacy": "public",
                        "interaction": null,
                        "firstComment": null,
                        "declarations": [
                          {
                            "field": "declarations.madeForKids",
                            "stated": null
                          },
                          {
                            "field": "declarations.containsSyntheticMedia",
                            "stated": false
                          }
                        ],
                        "refusal": {
                          "message": "YouTube requires `declarations.madeForKids` on every upload and this post does not state it"
                        }
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/posts/import": {
      "post": {
        "operationId": "postPostsImport",
        "summary": "Turn a spreadsheet into a calendar, all of it or none of it.",
        "description": "One CSV, up to 100 rows, in one transaction. The columns are `caption` and `videoUrl`, which every row must fill, plus the optional `scheduledTime` and `hashtags`; column order is free and a heading this endpoint does not know is a 400 rather than a column nobody reads. Every row is checked before anything is written — the caption against each platform that receives it, the video URL against the same rules `POST /posts` applies, the time against the calendar — and a single bad row imports nothing and comes back naming **every** row that is wrong, not the first. Imported posts are **held as drafts** unless you ask for `schedule: \"scheduled\"`, so uploading a file never publishes anything on its own.\n\nAll of the file or none of it. The rows are written in one transaction, so there is no state where some of your file is in the calendar and the rest is in your spreadsheet. The alternative — import the 99 and report the 1 — was weighed and rejected on the remedy rather than on the principle: a half-filled calendar is repaired by working out which rows landed and deleting them one at a time, which is the work this endpoint exists to remove. This one is repaired by fixing the line and uploading again.\n\nA rejected file answers `400 invalid_input` whose `message` is a header line followed by one line per bad row, each spelled `line {n}: {reason}`, in file order, with `{n}` counting physical lines from 1 so it is the number your editor shows. Every reason is a fixed sentence: no byte of your file is echoed back. The list is never truncated, because the row ceiling already bounds it.\n\nThe columns are `caption`, `videoUrl`, `scheduledTime` and `hashtags`. `caption` and `videoUrl` are required in the header and in every row; the other two are optional columns and a row may leave either cell empty. A duplicated heading is a 400, because which cell a row means stops being decidable. `hashtags` may be separated by spaces or by commas, whichever your sheet already uses.\n\nEvery row names its video by URL, and there is no way to import a post without one: this build cannot attach media to a post after it exists, so a caption-only row would sit in the calendar for ever unable to publish. Each URL is fetched into our own storage exactly as `POST /posts` fetches one, through the same guard and the same 200 MB limit, after the transaction commits. A URL that turns out not to be a video fails that post’s render and leaves the rest of the file alone.\n\nAt most 100 rows, and a file with more is a 400 naming the ceiling. The number is the work behind a row rather than the size of anybody’s spreadsheet: each row is a content insert, a post insert, up to three target inserts and a fetch of up to 200 MB, all while you wait. A quarter of daily content is about 90 rows.\n\nA file whose header names no data rows under it is a 400, as is an empty `csv`. Neither is treated as an import of nothing.\n\nHonours `Idempotency-Key`, and this is the endpoint where it matters most: it is the longest call this API serves, so a client timeout during the commit is the likeliest retry in the product, and a retry that repeated would be a second hundred posts.\n\nSixty requests per 300 seconds per key, shared with the rest of `/posts`. One call here is up to a hundred posts where `POST /posts` is one, so this bucket is a hundredfold louder than the rest of the group.\n\nScope: `posts:write`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "csv": {
                    "type": "string",
                    "description": "The file itself, as text, with its header row. Sent inside JSON rather than as a `text/csv` body because the accounts, the zone and the declarations travel with it and an object does not fit in a query string. The ceiling is 512 KB and a larger body is a 400 that tells you to split the file, never a file silently read to a cap."
                  },
                  "selectedAccounts": {
                    "type": "object",
                    "description": "Platform to account ids, exactly as on `POST /posts`. Every row in the file publishes to the same accounts: a file names captions, dates and videos, and there is no account column because that would be this product’s own UUIDs typed into a spreadsheet. Two sets of accounts are two uploads."
                  },
                  "schedule": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "scheduled"
                    ],
                    "description": "Whether the file goes live. `draft` — the default, and what you get by saying nothing — holds every post: they read `draft`, no publisher is queued for any of them, and each one goes out when you call `POST /posts/{id}/publish`. `scheduled` publishes each row at its own `scheduledTime`, and then every row must have one — a file with no `scheduledTime` column at all is refused once, naming the missing column, rather than once per row. There is no `asap`: a hundred posts sent at once is a pattern every platform penalises, and the one post you want out now is `POST /posts`.",
                    "default": "draft"
                  },
                  "timezone": {
                    "type": "string",
                    "description": "The IANA zone the file’s times are read in, and the zone the posts are stored in. **Required whenever the file has a `scheduledTime` column**, with no default in either direction: `2026-09-01 15:00` in a spreadsheet is a wall clock until somebody names the zone, and guessing one moves every row in the file by the same offset without saying so. A cell written as a full ISO 8601 instant carries its own offset and is read as itself. A file with no `scheduledTime` column needs no zone and the posts are stored in the workspace’s. IANA name, for example \"Europe/Berlin\"."
                  },
                  "declarations": {
                    "type": "object",
                    "description": "The same object `POST /posts` takes, judged by the same function, and one answer for the whole file: a declaration is about the account holder and how the video was made, and a file is one customer’s one kind of content. A file that names a YouTube account without `madeForKids` is a 400 at the door rather than a hundred refusals in a worker."
                  },
                  "validateOnly": {
                    "type": "boolean",
                    "description": "Read the file, answer with what it would create, and write nothing. Not a convenience: a file can be wholly valid and wholly wrong, and the commonest way to mis-schedule a quarter is a correct file read in the wrong zone, which passes every check this endpoint has. So every entry carries the **resolved UTC instant** of its row beside the line it came from, which is the one number a zone mistake shows up in before a hundred posts exist. `imported` is 0 and no entry carries an `id`, which is what makes a validation and a real import impossible to confuse.",
                    "default": false
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which brand these posts belong to. Defaults to the key’s default workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "required": [
                  "csv",
                  "selectedAccounts"
                ],
                "additionalProperties": false
              },
              "example": {
                "csv": "caption,videoUrl,scheduledTime,hashtags\nOne stockout costs more than a year of forecasting,https://cdn.example/stockout.mp4,2026-09-01 15:00,retail ops\nYour stockroom is lying to you,https://cdn.example/stockroom.mp4,2026-09-02 15:00,retail\n",
                "selectedAccounts": {
                  "tiktok": [
                    "ff9d429b-07c5-4320-a98c-ac6e1791b828"
                  ]
                },
                "schedule": "draft",
                "timezone": "Europe/Lisbon",
                "declarations": {
                  "aiGenerated": true
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "validateOnly": false,
                    "rows": 2,
                    "imported": 2,
                    "schedule": "draft",
                    "timezone": "Europe/Lisbon",
                    "posts": [
                      {
                        "line": 2,
                        "scheduledTime": "2026-09-01T14:00:00Z",
                        "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                        "status": "draft",
                        "statusUrl": "https://looptake.com/api/v1/posts/973821cf-5cc7-44f7-8795-e68da31c59ef"
                      },
                      {
                        "line": 3,
                        "scheduledTime": "2026-09-02T14:00:00Z",
                        "id": "0f8c4b6f-4d0b-4a09-9a51-b6d0f2a6a2f3",
                        "status": "draft",
                        "statusUrl": "https://looptake.com/api/v1/posts/0f8c4b6f-4d0b-4a09-9a51-b6d0f2a6a2f3"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/posts/{id}": {
      "get": {
        "operationId": "getPost",
        "summary": "Read a post with its per-platform results.",
        "description": "One target row per connected account, each with its own status, platform post id, public URL, error and analytics snapshot. A post is `partial` when some targets published and others did not, which is a state a single status field cannot express and a per-target row can.\n\nOne short link is minted per platform the post goes to, so `utm_source` answers \"how many came from TikTok\" rather than only \"how many came from this post\". Following one is a 302, never a 301: browsers cache a 301, which would stop the second click from ever being counted.\n\nTerminal for polling: `posted`, `partial`, `failed` and `cancelled`. A `renderStatus` of `failed` is terminal too. There is no `completed`: a post is done when every target it named has settled, and `partial` is the word for a mixed result.\n\n`firstComment` appears on a target only when one was written for that network, so a post composed without one reads exactly as it did before the field existed. Its `state` is `pending`, `posted`, `failed` or `unresolved`, and `needsAttention` is `true` for the last two — both mean the same thing to you: **the post is live and the comment is not**, so somebody has to add it by hand. The post is still `posted` in that case, deliberately: the video is on the account and re-publishing it would put it out twice, so the verdict on the comment sits beside the post rather than changing it. `unresolved` means one attempt was made and we never learned how it went; it is not retried, because a duplicate comment under your own video is worse than a missing one and neither network offers a way to ask. `text` carries what was meant to go under the post, so it can be copied straight across.\n\nAnalytics are refreshed on a schedule, so `fetchedAt` is part of the reading.\n\nScope: `posts:read`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The post id.",
            "schema": {
              "type": "string",
              "description": "The post id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                    "status": "partial",
                    "renderStatus": "ready",
                    "caption": "The maths nobody runs before they run out.",
                    "hashtags": [
                      "#retail",
                      "#inventory"
                    ],
                    "scheduledTime": "2026-09-01T15:00:00Z",
                    "declarations": {
                      "madeForKids": false,
                      "brandedContent": true,
                      "brandOrganic": null,
                      "aiGenerated": true,
                      "containsSyntheticMedia": true
                    },
                    "targets": [
                      {
                        "accountId": "ff9d429b-07c5-4320-a98c-ac6e1791b828",
                        "platform": "tiktok",
                        "status": "posted",
                        "platformPostId": "7412998812",
                        "postUrl": "https://www.tiktok.com/@northwind/video/7412998812",
                        "error": null,
                        "analytics": {
                          "views": 4820,
                          "likes": 311,
                          "comments": 24,
                          "shares": 61,
                          "saves": 88,
                          "fetchedAt": "2026-09-02T09:00:00Z"
                        }
                      },
                      {
                        "accountId": "1c74c860-000b-4136-88a7-a17f31019346",
                        "platform": "youtube",
                        "status": "failed",
                        "platformPostId": null,
                        "postUrl": null,
                        "error": "The account’s token expired. Reconnect and retry.",
                        "firstComment": {
                          "state": "failed",
                          "text": "#retail #inventory #ops",
                          "commentId": null,
                          "error": "the post published, but its first comment did not: youtube: this account was connected without the `https://www.googleapis.com/auth/youtube.force-ssl` permission, so it cannot post a first comment. Reconnect it and accept every permission, or remove the first comment from this post.",
                          "needsAttention": true
                        },
                        "analytics": null
                      }
                    ],
                    "attributionLinks": [
                      {
                        "id": "5e1e2ad1-9b56-4a0d-92b6-04f8dc3d19b6",
                        "slug": "k3Bq7Zra",
                        "url": "https://looptake.com/go/k3Bq7Zra",
                        "targetUrl": "https://northwind.example",
                        "utmSource": "tiktok",
                        "utmMedium": "social",
                        "utmCampaign": "looptake",
                        "clicks": 138,
                        "lastClickAt": "2026-09-02T08:41:03Z",
                        "createdAt": "2026-09-01T14:59:58Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patchPost",
        "summary": "Fix what a post says, and what you have declared about it, while it has not gone out.",
        "description": "Changes what a post says and what its account holder has declared about it, keeping the post. Without it a typo costs the id: DELETE and a second POST /posts mint a new one, and the approval, the short links already printed in the caption, the webhook deliveries your own system stored and the suggestion it was accepted from all end up pointing at a post that no longer exists. Accepted while the post is `draft`, `queued` or `scheduled` and no account has started; once a platform call is in flight the answer is 409, because the words are already on their way. Declarations were write-once until 2026-08-19: they were set at creation and no statement changed them, so a wrong AI disclosure cost the id that everything else here exists to keep.\n\nA body naming none of the four fields is 400 `invalid_input`. All four are replacements rather than merges: `hashtags` sets the whole list, `perPlatform` sets the whole set of overrides, `declarations` sets the whole object, and the way to add one is to send the collection you want.\n\nEditing an `approved` post puts it back to `pending`, and it is withheld until somebody agrees again. An approval is a judgement about specific words, so replacing the words replaces the thing that was agreed to — the same reason POST /posts/{id}/reschedule re-opens a decision, applied to what the post says rather than to when it says it. Re-sending the caption a post already has changes nothing and leaves the decision standing.\n\nA `pending` post keeps waiting, a `rejected` post keeps its refusal — rewriting is exactly what a refused post needs — and a post nobody was asked about is unaffected.\n\nThe publisher checks, as it claims each account, that the post still says what it is about to send. A post being uploaded right now refuses the edit; an edit that lands a moment earlier makes that attempt stand down and re-read, so the words that reach the platform are always the words that were stored.\n\nIt does not move the slot and it does not change which accounts a post goes to. POST /posts/{id}/reschedule is the first; the second is not an operation this API has. It does not replace the video either: no statement in this build writes a post’s media, so footage that is wrong is still a cancel and a new post.\n\nEditing a declaration on a LAUNCHED automation is still not possible: PATCH /automations/{id} answers 409 once a run has generated, and the posts it already scheduled keep the answers they were created with. Each of those posts is editable here, one at a time, which is the repair. There is no operation that restates a declaration across a whole run.\n\nA post that is `publishing`, `posted`, `partial`, `failed` or `cancelled` answers 409 `conflict`. A post you cannot read — no such id, or another team’s — is 404 `not_found`.\n\nA field this endpoint does not have is a 400, not a field we ignore. A misspelled key and a capability this build does not carry are both refused at the door, so `altText` and `hashtagPlacement` are refusals rather than fields we store and never send. `firstComment` was on that list until 2026-08-19 and is not any more: it is a real field now, inside `perPlatform`, on the two networks whose API can post a comment. At the TOP level it is still a 400, and deliberately so — a first comment is per network because one of the three cannot post one at all. The same rule already applied inside `perPlatform`; since 2026-08-19 it applies to the whole body.\n\nScope: `posts:write`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The post id.",
            "schema": {
              "type": "string",
              "description": "The post id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "caption": {
                    "type": "string",
                    "description": "The new caption. Omitting it leaves the caption alone; sending an empty string clears it. Checked against every platform this post targets, joined with the hashtags exactly as the publisher joins them, so a caption that fits Instagram and not TikTok fails the request rather than one account."
                  },
                  "hashtags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The new hashtags, replacing the whole list. Omitting the field leaves them alone; sending `[]` clears them. They count towards the platform caption limit, because the platform receives one string."
                  },
                  "perPlatform": {
                    "type": "object",
                    "description": "The per-platform words, as on `POST /posts`. Different rule from the two fields above, and it is the only way clearing an override can be expressed: sending this field **replaces the whole set**. A platform named in it is written as named, a platform left out of it goes back to publishing the post’s own caption, and `{}` clears every platform at once. Omitting the field entirely leaves every platform’s words alone. The text each account will receive is re-checked against that account’s own limits, and changing any of it re-opens an approved post for exactly the reason changing the caption does. That includes `firstComment`: because this field replaces the whole set, a client that reads a post, edits its caption and sends back a `perPlatform` without the `firstComment` it read has DELETED that comment. Send back what you read."
                  },
                  "declarations": {
                    "type": "object",
                    "description": "The five statements the platforms hold the account holder to, as on `POST /posts`. **Sending this field replaces the whole object**: a key you leave out is written as *not stated*, not left alone, which is the same rule `PATCH /automations/{id}` has always applied to the same object. That is forced rather than chosen, because not stated is one of three answers and the one the wire spells as an absent key: a merge would leave you no way to withdraw a `brandedContent` you ticked by mistake, and withdrawing a wrong answer is the only thing editing a declaration is ever for. Omitting `declarations` entirely leaves all five alone. `madeForKids` is still required for a post that names a YouTube account, judged against the accounts this post already has, so an edit cannot withdraw the one declaration a platform refuses an upload without. Changing any stored answer re-opens an approved post, for a sharper version of the reason changing the caption does: a reviewer agreed to a post that disclosed something. Available since 2026-08-19. Before that a declaration was write-once, and a wrong one could only be fixed by cancelling the post and creating another with a new id."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "caption": "The maths nobody runs before they run out of stock."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                    "status": "scheduled",
                    "caption": "The maths nobody runs before they run out of stock.",
                    "hashtags": [
                      "#retail",
                      "#inventory"
                    ],
                    "perPlatform": {
                      "tiktok": {
                        "caption": "Your stockroom is lying to you",
                        "hashtags": null,
                        "title": null
                      }
                    },
                    "declarations": {
                      "madeForKids": null,
                      "brandedContent": null,
                      "brandOrganic": null,
                      "aiGenerated": true,
                      "containsSyntheticMedia": null
                    },
                    "approval": null,
                    "updatedAt": "2026-08-18T14:22:09Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deletePost",
        "summary": "Cancel a scheduled post.",
        "description": "Only while the post is `draft`, `queued` or `scheduled`, **and** no account has started: the guard is a `not exists` over the target rows, not a check on the post's own status, because `posts.status` still reads `scheduled` for the whole of a platform round trip. Once any target is in flight the call is already out and the answer is 409.\n\nAny other status returns 409 `conflict`. The content piece is untouched and can be scheduled again.\n\nScope: `posts:write`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The post id.",
            "schema": {
              "type": "string",
              "description": "The post id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                    "status": "cancelled",
                    "cancelledAt": "2026-08-12T11:20:44Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/posts/{id}/approve": {
      "post": {
        "operationId": "postPostApprove",
        "summary": "Approve a post that is waiting for review.",
        "description": "Releases a post whose `approval.status` is `pending`, and queues its publish immediately, so a slot approved after its scheduled time still goes out rather than being lost. Behind `posts:approve`, which `posts:write` deliberately does not imply: the credential that schedules a post is not always the one that may release it. There is no un-approving as an operation, because that would be a lie once the publisher holds the post, and `DELETE /posts/{id}` is the one that honestly stops one. Two things re-open an approved decision and neither is a decision: `POST /posts/{id}/reschedule` puts the post back to `pending` because a reviewer decides in front of a slot, and `PATCH /posts/{id}` does the same when it changes what the post says, because an approval is a judgement about specific words.\n\nAn API key cannot decide, whatever scopes it holds: the call answers 403 `forbidden_scope`. A decision has to name a person and a key acts for a team, so a null decider would put an approval on the record nobody can be asked about. Decisions come from a signed-in member holding `posts:approve`, or from an external reviewer link.\n\nA post nobody was asked about, whose `approval` is `null` rather than `pending`, is 409 `conflict` rather than 404: the post exists, you can read it, and it is the state that refuses.\n\nA post that is already publishing or published is 409. So is approving a `rejected` post: re-open it with `POST /posts/{id}/reject` first, which puts it back to `pending`.\n\nScope: `posts:approve`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The post id.",
            "schema": {
              "type": "string",
              "description": "The post id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "A note kept on the decision and shown to whoever scheduled the post. Up to 500 characters. Omitting it on an approval leaves no note: the field is not carried over from an earlier decision, because a note written to explain a refusal would then be reported as the approver’s own words. Sending `\"\"` clears the stored note explicitly."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "reason": "Checked the claim in line two against the pricing page."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                    "status": "scheduled",
                    "approval": {
                      "status": "approved",
                      "decidedAt": "2026-08-13T09:12:44Z",
                      "decidedBy": "b4a0d51c-6f7e-42a8-9c31-8d5f0a7be213",
                      "note": "Checked the claim in line two against the pricing page.",
                      "reviewLinkId": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/posts/{id}/reject": {
      "post": {
        "operationId": "postPostReject",
        "summary": "Reject a pending post, or re-open a rejected one.",
        "description": "Refuses a `pending` post, which keeps its slot and its row and simply never publishes. Sent against a post that is already `rejected`, it re-opens it: the decision goes back to `pending` and through the queue again. That is the only route back, because there is no transition from `rejected` straight to `approved`. Overturning a refusal in one write would leave no trace that a refusal ever happened.\n\nSame credential rule as approve: a key is 403 `forbidden_scope`, because a decision has to name a person.\n\nThe second call on a rejected post answers `\"status\": \"pending\"`. It is the same endpoint on purpose: a separate re-open route would be a second way to spell one transition.\n\nNothing is cancelled or deleted. A rejected post keeps its calendar slot, and the publisher skips it with the reason attached.\n\nScope: `posts:approve`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The post id.",
            "schema": {
              "type": "string",
              "description": "The post id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Why it was refused. This is the sentence the author reads, so it is worth writing. Up to 500 characters. On a re-open (this endpoint sent against an already-rejected post) omitting it keeps the refusal’s reason rather than erasing it, so the post goes back into the queue with what was said about it still attached. Sending `\"\"` clears the stored note explicitly, which is how a note that should not have been written is retracted."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "reason": "The claim in line two is not one we can make."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                    "status": "scheduled",
                    "approval": {
                      "status": "rejected",
                      "decidedAt": "2026-08-13T09:14:02Z",
                      "decidedBy": "b4a0d51c-6f7e-42a8-9c31-8d5f0a7be213",
                      "note": "The claim in line two is not one we can make.",
                      "reviewLinkId": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/posts/{id}/reschedule": {
      "post": {
        "operationId": "postPostReschedule",
        "summary": "Move a scheduled post to a different time.",
        "description": "Changes when a post goes out, keeping the post. Without this the only way to change a slot is DELETE and a second POST /posts, which mints a new id and drops everything pointing at the old one — the short links already printed in a caption, the approval a reviewer gave, the suggestion it was accepted from. Accepted while the post is `draft`, `queued` or `scheduled`; once it is `publishing` the platform call is in flight and the answer is 409. The publisher queued for the old instant is moved with the row, in the same transaction, so a post pulled forward goes out at the new time rather than the old one.\n\nMoving a post whose `approval.status` is `approved` puts it back to `pending`, and the post is withheld until somebody agrees again. A reviewer decides in front of the slot, so a yes is a yes to this post at this time; the alternative — cancel and recreate — needs a fresh approval too and loses the post’s id as well. The decision fields are cleared and `note` records the move.\n\nA `pending` post keeps waiting and a `rejected` post keeps its refusal. A post nobody was asked about, whose `approval` is `null`, is unaffected.\n\nAn `asap` post — one created without a `scheduledTime`, which reads as `queued` — becomes `scheduled` at the instant you name.\n\nA `draft` is moved and **stays a draft**. Giving a post a date is something you do while still writing it, so this call writes the instant and leaves the hold in place; `POST /posts/{id}/publish` is what releases it, and it then goes out at the instant this call set.\n\nA post that is `publishing`, `posted`, `partial`, `failed` or `cancelled` answers 409 `conflict`: the post exists and you can read it, and it is the state that refuses. A post you cannot read — no such id, or another team’s — is 404 `not_found`, the same answer every other read of it gives.\n\nScope: `posts:write`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The post id.",
            "schema": {
              "type": "string",
              "description": "The post id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scheduledTime": {
                    "type": "string",
                    "description": "The new instant, ISO 8601. Must be in the future: a post moved into the past publishes immediately, which is not what you asked for and is not something you can undo.",
                    "format": "date-time"
                  },
                  "timezone": {
                    "type": "string",
                    "description": "The IANA zone the new time was chosen in, for example `Europe/Lisbon`. Omitting it keeps the zone the post already carries — which is not the same as sending the workspace default, because the zone is what makes “9am” still mean 9am after the clocks change."
                  }
                },
                "required": [
                  "scheduledTime"
                ],
                "additionalProperties": false
              },
              "example": {
                "scheduledTime": "2026-09-04T09:00:00Z",
                "timezone": "Europe/Lisbon"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                    "status": "scheduled",
                    "scheduledTime": "2026-09-04T09:00:00Z",
                    "timezone": "Europe/Lisbon",
                    "approval": null,
                    "updatedAt": "2026-08-18T10:41:07Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/posts/{id}/publish": {
      "post": {
        "operationId": "postPostPublish",
        "summary": "Release a draft.",
        "description": "Stops holding a post you saved unfinished, so it goes out at the time it already carries. This is the only thing that releases a draft: no sweep, no worker and no other endpoint moves a post off `draft`, which is what makes `schedule: \"draft\"` on POST /posts a saved post rather than one that quietly publishes itself.\n\nIt takes no body and it does not choose a time. The draft already answered that question when it was created — a draft carrying a `scheduledTime` becomes `scheduled` and publishes at that instant, and one carrying none becomes `queued` and publishes as soon as its render finishes. POST /posts/{id}/reschedule is the operation that sets a slot, and it deliberately leaves a draft held, so the two never write the same column for the same click.\n\n`posts:write` and not `posts:approve`. Deciding a draft is finished is the author’s act, and it does not bypass a reviewer: a draft released in a workspace that requires approval lands `pending` and waits, exactly as a scheduled post does. Read `approval` on the response before telling anybody it is going out.\n\nA post that is not a draft answers 409 `conflict` — including a draft you have already released, which is what makes a double press safe. A post you cannot read is 404 `not_found`, the same answer every other read of it gives.\n\nThe publisher is queued here only when there is one to queue: a draft still rendering has a publish job waiting on its render already, and it picks the post up when the video lands.\n\nScope: `posts:write`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The post id.",
            "schema": {
              "type": "string",
              "description": "The post id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                    "status": "queued",
                    "scheduledTime": null,
                    "timezone": "Europe/Lisbon",
                    "approval": null,
                    "updatedAt": "2026-08-19T10:41:07Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/calendar": {
      "get": {
        "operationId": "getCalendar",
        "summary": "Read the schedule as days.",
        "description": "The same posts as GET /posts, grouped by local day in the workspace timezone rather than returned as a flat list. This is the shape a calendar view needs, and it is a separate endpoint so a month view is one request instead of a paginated crawl.\n\n`from` and `to` are both required. An unbounded calendar request is rejected as `invalid_input`.\n\nEach entry carries `approvalStatus` beside `status`, because they are different axes and a month drawn from `status` alone shows `scheduled` for posts that will not publish. `null` means no approval was demanded of that post. Four fields per entry and no more: open `GET /posts/{id}` for the caption, the targets and who decided.\n\nScope: `posts:read`.\n\nRate limit: 60 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "posts"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "First day, inclusive. ISO date.",
            "schema": {
              "type": "string",
              "description": "First day, inclusive. ISO date.",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "description": "Last day, inclusive. ISO date.",
            "schema": {
              "type": "string",
              "description": "Last day, inclusive. ISO date.",
              "format": "date"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filters the posts inside each day.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "queued",
                "scheduled",
                "publishing",
                "posted",
                "partial",
                "failed",
                "cancelled"
              ],
              "description": "Filters the posts inside each day."
            }
          },
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "timezone": "Europe/Lisbon",
                    "days": [
                      {
                        "date": "2026-09-01",
                        "posts": [
                          {
                            "id": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                            "scheduledTime": "2026-09-01T15:00:00Z",
                            "status": "scheduled",
                            "approvalStatus": "pending"
                          }
                        ]
                      },
                      {
                        "date": "2026-09-02",
                        "posts": []
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/automations": {
      "post": {
        "operationId": "postAutomations",
        "summary": "Create an automation.",
        "description": "An automation is a cadence plus a target set. Creating one leaves it in `draft`: nothing is generated and nothing is scheduled until you call generate, and nothing publishes until you call launch. That three-step shape is what makes a month of posts reviewable rather than a thing that started while you were reading about it.\n\nScope: `automations:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "What this run is called in the app and in the ledger."
                  },
                  "cadence": {
                    "type": "object",
                    "description": "postsPerDay, days, startDate, times[] and timezone. Defaults to one post a day for seven days from tomorrow."
                  },
                  "selectedAccounts": {
                    "type": "object",
                    "description": "Platform to account ids, the same shape POST /posts takes."
                  },
                  "settings": {
                    "type": "object",
                    "description": "Format mix, prompt and caption preferences applied to every post in the run."
                  },
                  "ownerType": {
                    "type": "string",
                    "enum": [
                      "brand",
                      "influencer"
                    ],
                    "description": "Whose accounts the run publishes from.",
                    "default": "brand"
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Required when `ownerType` is `influencer`.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "tiktokMode": {
                    "type": "string",
                    "enum": [
                      "direct",
                      "inbox"
                    ],
                    "description": "Publish to TikTok directly, or send every post to drafts. The automation-wide form of `tiktokSendToInbox`, and it inherits the same caveat: an inbox draft reaches TikTok without its caption, so a month of inbox posts is a month of captions to paste in by hand.",
                    "default": "direct"
                  },
                  "tiktokOptions": {
                    "type": "object",
                    "description": "Per-run TikTok publishing options. `disableComment`, `disableDuet` and `disableStitch`, or the permissive spellings `allowComments`, `allowDuet` and `allowStitch`. Anything you do not name is left at the account default."
                  },
                  "declarations": {
                    "type": "object",
                    "description": "The platform declarations every post this run schedules is created with: `madeForKids`, `brandedContent`, `brandOrganic`, `aiGenerated` and `containsSyntheticMedia`, the same five `POST /posts` takes. Stated here because the posts are created weeks later with nobody present to ask, and a plan can only be edited while the run is a draft. A run launched without the two synthetic-media answers publishes a month of video that declares nothing about how it was made, and there is no later moment to add them *to the run*: `PATCH /automations/{id}` answers 409 once a run has generated. Since 2026-08-19 there is a later moment for each POST it scheduled, one at a time, through `PATCH /posts/{id}`, which is a repair rather than a plan and is per slot. A run whose `selectedAccounts` include YouTube and whose `madeForKids` is unstated is refused at launch rather than at each slot’s own publish time; the other four are never a refusal."
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which brand the run belongs to. Defaults to the key’s default workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "required": [
                  "name"
                ],
                "additionalProperties": false
              },
              "example": {
                "name": "September daily",
                "cadence": {
                  "postsPerDay": 1,
                  "days": 30,
                  "startDate": "2026-09-01",
                  "times": [
                    "15:00"
                  ],
                  "timezone": "Europe/Lisbon"
                },
                "selectedAccounts": {
                  "tiktok": [
                    "ff9d429b-07c5-4320-a98c-ac6e1791b828"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                    "name": "September daily",
                    "status": "draft",
                    "cadence": {
                      "postsPerDay": 1,
                      "days": 30,
                      "startDate": "2026-09-01",
                      "times": [
                        "15:00"
                      ],
                      "timezone": "Europe/Lisbon"
                    },
                    "ownerType": "brand",
                    "createdAt": "2026-08-12T11:25:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getAutomations",
        "summary": "List automations.",
        "description": "Every automation the workspace holds, with its status and its cadence. The status tells you which call is legal next: `draft` takes generate, `review` takes launch, `launched` takes extend, pause or cancel, and `paused` takes resume or cancel.\n\nScope: `automations:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "One-based page number.",
            "schema": {
              "type": "integer",
              "description": "One-based page number.",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "description": "Items per page.",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                      "name": "September daily",
                      "status": "launched",
                      "cadence": {
                        "postsPerDay": 1,
                        "days": 30,
                        "startDate": "2026-09-01",
                        "times": [
                          "15:00"
                        ],
                        "timezone": "Europe/Lisbon"
                      },
                      "postCount": 30,
                      "createdAt": "2026-08-12T11:25:00Z"
                    }
                  ],
                  "pagination": {
                    "total": 3,
                    "page": 1,
                    "limit": 1,
                    "pages": 3
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/automations/{id}": {
      "get": {
        "operationId": "getAutomation",
        "summary": "Read one automation.",
        "description": "Adds the settings, the selected accounts and the counts the list shape leaves out: how many posts were generated, how many are still awaiting review and how many have published. It also returns all five of the run’s `declarations`, which you need before you PATCH: that call replaces the object as a whole, so an editor that cannot read the stored answer would have to invent one, and an unstated `madeForKids` reads back as `null` rather than as `false`.\n\nScope: `automations:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The automation id.",
            "schema": {
              "type": "string",
              "description": "The automation id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                    "name": "September daily",
                    "status": "launched",
                    "cadence": {
                      "postsPerDay": 1,
                      "days": 30,
                      "startDate": "2026-09-01",
                      "times": [
                        "15:00"
                      ],
                      "timezone": "Europe/Lisbon"
                    },
                    "selectedAccounts": {
                      "tiktok": [
                        "ff9d429b-07c5-4320-a98c-ac6e1791b828"
                      ]
                    },
                    "settings": {
                      "formats": [
                        "walloftext",
                        "slideshow"
                      ],
                      "mentionBusiness": true
                    },
                    "ownerType": "brand",
                    "influencerId": null,
                    "tiktokMode": "direct",
                    "declarations": {
                      "madeForKids": null,
                      "brandedContent": true,
                      "brandOrganic": null,
                      "aiGenerated": true,
                      "containsSyntheticMedia": null
                    },
                    "counts": {
                      "generated": 30,
                      "scheduled": 28,
                      "posted": 2,
                      "failed": 0
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patchAutomation",
        "summary": "Edit a draft automation.",
        "description": "Only while the automation is in `draft`. Once it has generated posts, editing the cadence would mean rewriting a schedule a person has already reviewed, so the call is a 409 and the correct move is to cancel and create a new one.\n\nAny status other than `draft` returns 409 `conflict`.\n\nScope: `automations:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The automation id.",
            "schema": {
              "type": "string",
              "description": "The automation id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Rename the run."
                  },
                  "cadence": {
                    "type": "object",
                    "description": "Replaces the cadence object wholesale."
                  },
                  "selectedAccounts": {
                    "type": "object",
                    "description": "Replaces the target set."
                  },
                  "settings": {
                    "type": "object",
                    "description": "Replaces the settings object."
                  },
                  "tiktokMode": {
                    "type": "string",
                    "enum": [
                      "direct",
                      "inbox"
                    ],
                    "description": "Switches the run between publishing and drafts."
                  },
                  "tiktokOptions": {
                    "type": "object",
                    "description": "Replaces the TikTok options object."
                  },
                  "declarations": {
                    "type": "object",
                    "description": "Replaces the declarations object as a whole, including any field you set back to null. Omit it and this patch leaves the run’s declarations alone."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "cadence": {
                  "postsPerDay": 2,
                  "days": 30,
                  "startDate": "2026-09-01",
                  "times": [
                    "09:00",
                    "18:00"
                  ],
                  "timezone": "Europe/Lisbon"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                    "status": "draft",
                    "cadence": {
                      "postsPerDay": 2,
                      "days": 30,
                      "startDate": "2026-09-01",
                      "times": [
                        "09:00",
                        "18:00"
                      ],
                      "timezone": "Europe/Lisbon"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/automations/{id}/generate": {
      "post": {
        "operationId": "postAutomationGenerate",
        "summary": "Fill the automation with posts.",
        "description": "Moves the automation to `generating`, writes one suggestion per slot in the cadence, and lands it in `review`. Nothing is published by this call. Poll GET /automations/{id} until the status leaves `generating`.\n\nTerminal statuses for this poll: `review`, or `cancelled` if the run was cancelled mid-generation.\n\nScope: `automations:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The automation id.",
            "schema": {
              "type": "string",
              "description": "The automation id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                    "status": "generating",
                    "slots": 30,
                    "statusUrl": "https://looptake.com/api/v1/automations/4ecde50a-7af3-43b9-b66e-a36422c7e51f"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/automations/{id}/extend": {
      "post": {
        "operationId": "postAutomationExtend",
        "summary": "Add days to a running automation.",
        "description": "Extends the cadence past its original end and generates the new slots at the same rhythm. This is how a run becomes continuous without a second automation and without a gap in the calendar.\n\nScope: `automations:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The automation id.",
            "schema": {
              "type": "string",
              "description": "The automation id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "days": {
                    "type": "integer",
                    "description": "How many days to add after the current last day.",
                    "minimum": 1
                  }
                },
                "required": [
                  "days"
                ],
                "additionalProperties": false
              },
              "example": {
                "days": 14
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                    "status": "generating",
                    "days": 44,
                    "addedSlots": 14
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/automations/{id}/launch": {
      "post": {
        "operationId": "postAutomationLaunch",
        "summary": "Launch a reviewed automation.",
        "description": "Schedules every post the run generated and moves it to `launched`. This is the call that publishes, so it honours `Idempotency-Key`: a retried launch after a timeout replays the stored response rather than scheduling the month twice.\n\nOnly from `review`. Launching a draft returns 409 `conflict`.\n\nA run whose `selectedAccounts` include YouTube must carry `declarations.madeForKids` before it can launch, and a 400 here names the field. The check is at launch because this is the last request before a month of posts exists: after it, every slot is created by a background job with nobody left to ask.\n\nScope: `automations:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The automation id.",
            "schema": {
              "type": "string",
              "description": "The automation id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                    "status": "launched",
                    "scheduled": 30,
                    "firstPostAt": "2026-09-01T15:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/automations/{id}/pause": {
      "post": {
        "operationId": "postAutomationPause",
        "summary": "Hold a launched automation.",
        "description": "Stops a running cadence without throwing its calendar away. Nothing is cancelled: every post keeps its row, its slot and the publisher already queued for it, and each one is withheld at the moment it would go out rather than being deleted now. Resuming moves the rest of the calendar forward by however long the hold lasted, so a run held for a week comes back a week later in the same rhythm.\n\nOnly from `launched`. A run that has not launched has no calendar to hold, and a 409 says so.\n\nNever refused because the workspace is archived. Stopping is never blocked: a guard that would not let you hold a run inside an archived brand would trap you in the state you were leaving.\n\nScope: `automations:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The automation id.",
            "schema": {
              "type": "string",
              "description": "The automation id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                    "status": "paused",
                    "heldPosts": 22,
                    "pausedAt": "2026-09-08T11:04:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/automations/{id}/resume": {
      "post": {
        "operationId": "postAutomationResume",
        "summary": "Give a held automation back.",
        "description": "Moves every post the run still had waiting forward by exactly the length of the hold and puts the run back to `launched`. The spacing is preserved, so two posts a day is still two a day. Resuming onto the original instants would publish every slot that passed during the hold at once, which is the thing a hold is pressed to prevent. `heldForSeconds` is what the calendar moved by.\n\nOnly from `paused`. Anything else is a 409, including a second resume: the move is a compare-and-set, so two resumes in flight shift the calendar once.\n\nRefused with a 409 while the workspace is archived, because a resumed post in an archived brand is withheld again at its own slot. Restore the brand first.\n\nRefused with a 409 when the run belongs to an influencer whose consent has been withdrawn since it launched. The videos were rendered before the hold, so this is the last check between a withdrawn likeness and an audience.\n\nScope: `automations:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The automation id.",
            "schema": {
              "type": "string",
              "description": "The automation id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                    "status": "launched",
                    "resumedPosts": 22,
                    "heldForSeconds": 604800,
                    "firstPostAt": "2026-09-16T15:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/automations/{id}/cancel": {
      "post": {
        "operationId": "postAutomationCancel",
        "summary": "Cancel an automation.",
        "description": "Cancels the run and every post it scheduled that has not published yet. Posts that already went out are left alone, because unpublishing is not something an API can promise on three platforms.\n\nScope: `automations:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The automation id.",
            "schema": {
              "type": "string",
              "description": "The automation id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                    "status": "cancelled",
                    "cancelledPosts": 28,
                    "keptPosts": 2
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/automations/{id}/posts": {
      "get": {
        "operationId": "getAutomationPosts",
        "summary": "List the posts an automation produced.",
        "description": "The review queue while the run is in `review`, and the run’s history after it launches. These posts are deliberately excluded from GET /generations, so this is the only way to read them.\n\nScope: `automations:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The automation id.",
            "schema": {
              "type": "string",
              "description": "The automation id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filters by post status. `draft` is a post saved unfinished that nothing will publish; `queued` is one going out as soon as its render finishes. Until 2026-08-19 there was one value called `draft` and it meant the second of those.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "queued",
                "scheduled",
                "publishing",
                "posted",
                "partial",
                "failed",
                "cancelled"
              ],
              "description": "Filters by post status. `draft` is a post saved unfinished that nothing will publish; `queued` is one going out as soon as its render finishes. Until 2026-08-19 there was one value called `draft` and it meant the second of those."
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "One-based page number.",
            "schema": {
              "type": "integer",
              "description": "One-based page number.",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "description": "Items per page.",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "98653bae-8565-4cac-a3b8-7fb296ca356d",
                      "status": "scheduled",
                      "format": "slideshow",
                      "scheduledTime": "2026-09-03T15:00:00Z",
                      "caption": "Three numbers that decide your reorder point."
                    }
                  ],
                  "pagination": {
                    "total": 30,
                    "page": 1,
                    "limit": 1,
                    "pages": 30
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/influencers": {
      "get": {
        "operationId": "getInfluencers",
        "summary": "List AI influencers.",
        "description": "The influencers this workspace has created, with the attributes that describe them and the voice each one speaks with. The plan caps how many a team may hold.\n\nScope: `influencers:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "influencers"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Matches name, description and tags.",
            "schema": {
              "type": "string",
              "description": "Matches name, description and tags."
            }
          },
          {
            "name": "niche",
            "in": "query",
            "required": false,
            "description": "Filters to one niche.",
            "schema": {
              "type": "string",
              "description": "Filters to one niche."
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "One-based page number.",
            "schema": {
              "type": "integer",
              "description": "One-based page number.",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "description": "Items per page.",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "d076f17e-4529-48a3-b5fe-860bc329c5b4",
                      "name": "Mara",
                      "imageUrl": "https://cdn.looptake.com/d076f17e-4529-48a3-b5fe-860bc329c5b4/portrait.jpg",
                      "description": "Shop owner turned operations nerd.",
                      "niche": "retail",
                      "gender": "female",
                      "age": 34,
                      "ethnicity": "mixed",
                      "tags": [
                        "retail",
                        "operations"
                      ],
                      "voiceId": "voice_warm_en_3",
                      "status": "ready",
                      "consentStatus": "attested",
                      "consentPermitsRender": true
                    }
                  ],
                  "pagination": {
                    "total": 4,
                    "page": 1,
                    "limit": 1,
                    "pages": 4
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postInfluencers",
        "summary": "Create an AI influencer.",
        "description": "Renders a preview of the character from the reference image and the attributes, and costs 10 credits. The influencer is reusable after that: every video you render from it costs per second, not per character. `subject` is optional in the schema and decisive in effect: a body that does not say what the image is gets treated as `real_person`, which is the strict branch, and a `real_person` with no `consent` block is refused with 400 before anything is charged. Send `subject: \"synthetic\"` for a wholly invented character and nothing else is needed; send `real_person` and attest, with the release lodged through POST /influencers/consent-artefacts first. The permissive default was the alternative, and it would have made the whole control opt-in.\n\nCosts 10 credits. A failed preview is refunded automatically by the worker that detected it.\n\nA body with no `subject` is read as `real_person` and refused with 400 `invalid_input` when it carries no `consent`. This is a change: a script written before consent tooling shipped, which sent only `name` and `imageUrl`, now fails. Nothing is charged by the refusal.\n\n`consent.artefact` names an upload that must already exist. The object is `head`ed and read once to record its SHA-256, so an id whose upload never happened is refused rather than attached to an attestation.\n\n`consentStatus` is `attested` for an attested real person, `not_required` for a synthetic one, and `null` only on rows created before this control existed. Recording an attestation is not verification of the subject: nothing here contacts the person in the photograph.\n\nScope: `influencers:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts 10 credits per influencer.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.",
        "tags": [
          "influencers"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "What the character is called."
                  },
                  "imageUrl": {
                    "type": "string",
                    "description": "A reference portrait. The face every later render is built from.",
                    "format": "uri"
                  },
                  "subject": {
                    "type": "string",
                    "enum": [
                      "synthetic",
                      "real_person"
                    ],
                    "description": "What the portrait is: a wholly invented character, or a likeness of a real human being. Not required, and that is the control rather than a gap in it: absence means `real_person`, so omitting the field does not skip the question, it answers it the strict way and then demands a `consent` block. Publishing it as required would have made a validating client refuse to send the one body that answers most strictly. A near miss such as `realperson` is 400 rather than being folded into either branch."
                  },
                  "consent": {
                    "type": "object",
                    "description": "Required when `subject` is `real_person`. Send `{ statement, subjectName?, artefact: { id, filename, kind } }`: your own words for what the subject agreed to, and the release you lodged with POST /influencers/consent-artefacts. Cannot be sent with a `synthetic` subject, because an invented character has nobody to hold a release from."
                  },
                  "description": {
                    "type": "string",
                    "description": "Who they are. Feeds the scripts written in their voice."
                  },
                  "niche": {
                    "type": "string",
                    "description": "The subject they speak about."
                  },
                  "gender": {
                    "type": "string",
                    "description": "Presented gender, used for voice selection."
                  },
                  "age": {
                    "type": "integer",
                    "description": "Apparent age in years."
                  },
                  "ethnicity": {
                    "type": "string",
                    "description": "Apparent ethnicity, used for character consistency across renders."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Your own labels, searchable through `search`."
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which brand the character belongs to. Defaults to the key’s default workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "required": [
                  "name",
                  "imageUrl"
                ],
                "additionalProperties": false
              },
              "example": {
                "name": "Mara",
                "imageUrl": "https://cdn.example.com/mara.jpg",
                "subject": "real_person",
                "consent": {
                  "subjectName": "Mara Okonkwo",
                  "statement": "Signed likeness release, worldwide, 12 months, on file.",
                  "artefact": {
                    "id": "3f8a6c21-0d94-4f7a-9c62-51a0b7e4d8f3",
                    "filename": "release.pdf",
                    "kind": "written_release"
                  }
                },
                "description": "Shop owner turned operations nerd.",
                "niche": "retail",
                "tags": [
                  "retail",
                  "operations"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "d076f17e-4529-48a3-b5fe-860bc329c5b4",
                    "name": "Mara",
                    "status": "ready",
                    "voiceId": "voice_warm_en_3",
                    "subject": "real_person",
                    "consentStatus": "attested",
                    "creditsSpent": 10,
                    "createdAt": "2026-08-12T11:31:12Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/influencers/consent-artefacts": {
      "post": {
        "operationId": "postInfluencerConsentArtefact",
        "summary": "Get an upload URL for a release or a recorded statement.",
        "description": "The first call of the real-person flow: it returns a presigned `PUT` URL, you upload the document to it, and then you name the returned `artefactId` in the `consent` block of POST /influencers or POST /influencers/{id}/consent. Nothing is recorded here and no credits are charged, because an artefact is not evidence of anything until an attestation points at it, and charging for the upload would put a price on the compliant path. The declared `bytes` is signed into the URL, so the bucket itself refuses a body of any other length.\n\nThe URL is good for 30 minutes. One left in a browser tab overnight is dead, because a URL that outlives the sitting it was minted in is a standing write grant to an evidentiary prefix.\n\nUp to 32 MiB (33,554,432 bytes). The cap is what this endpoint will read back to digest, not what a bucket can hold.\n\nThe object is stored under a retention class that never expires and that the storage layer refuses to delete, and it has no public URL. Evidence that expires with the media is not evidence.\n\nScope: `influencers:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "influencers"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "written_release",
                      "recorded_statement"
                    ],
                    "description": "What the document is. A `written_release` must be a pdf, png, jpg, jpeg or webp; a `recorded_statement` must be an mp4, webm, mov, m4a, mp3 or wav."
                  },
                  "filename": {
                    "type": "string",
                    "description": "A plain name with its extension, using letters, digits, `.`, `_` and `-` only. The extension has to match `kind`."
                  },
                  "bytes": {
                    "type": "integer",
                    "description": "The exact size of the file. Signed into the upload URL, so a body of a different length is refused by the bucket rather than discovered afterwards.",
                    "minimum": 1,
                    "maximum": 33554432
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which brand the likeness belongs to. Defaults to the key’s default workspace, and has to be the same workspace as the influencer the artefact is attached to.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "required": [
                  "kind",
                  "filename",
                  "bytes"
                ],
                "additionalProperties": false
              },
              "example": {
                "kind": "written_release",
                "filename": "release.pdf",
                "bytes": 184320
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "artefactId": "3f8a6c21-0d94-4f7a-9c62-51a0b7e4d8f3",
                    "filename": "release.pdf",
                    "kind": "written_release",
                    "bytes": 184320,
                    "uploadUrl": "https://uploads.looptake.com/invoice/e7293116-fd57-4456-8da0-032f4976145f/91e14cbf-2c80-4b3b-a6bf-776bbde6b54c/consent/3f8a6c21-0d94-4f7a-9c62-51a0b7e4d8f3/release.pdf?sig=…",
                    "contentType": "application/pdf",
                    "expiresAt": "2026-08-12T12:01:12Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/influencers/{id}/consent": {
      "post": {
        "operationId": "postInfluencerConsent",
        "summary": "Attest, withdraw or refuse consent for a likeness.",
        "description": "Records one decision about an influencer that already exists, append-only: `attest` needs a statement and a lodged artefact, while `withdraw` and `refuse` need a statement and no document at all, because the stricter direction has to be the easier one to take. They may still carry one, and it is stored: a revocation letter is real evidence and belongs on the record rather than in an inbox. From a withdrawal or a refusal, every path that would spend credits on the likeness answers 409: a new video, a studio render carrying `influencerId`, a post published as that influencer, creating an automation for it, and launching one. Consent can be given again later with a fresh artefact, and the history keeps both records.\n\n`artefact` is required only when `action` is `attest`. A withdrawal or a refusal may carry one and it is stored exactly as an attestation’s is: verified against the bucket, measured, digested, and filed under the prefix that refuses deletion. A revocation letter or an email saying no is real evidence of why consent ended, and an earlier release accepted the field on those two actions and silently discarded it.\n\nDemanding paperwork before we will stop using somebody’s likeness would make the stricter direction the harder one to take, so `artefact` stays optional on `withdraw` and `refuse` and always will be. Sending one that cannot be verified is a 400 and writes nothing at all, rather than half-recording the decision; the message says so, and re-sending the same request without `artefact` records the withdrawal immediately.\n\n`runningAutomations` is what a withdrawal deliberately does not stop. Launched runs keep publishing this likeness on their own timetable until you cancel each one with POST /automations/{id}/cancel. Cancelling a month of your calendar as a side effect of a status change is a decision that belongs to you, and it is one you cannot undo.\n\nMedia already published stays live, keeps its analytics and keeps naming the influencer, so the set that needs a decision is listable.\n\nThe whole history is on GET /influencers/{id} as `consentRecords`, and each decision is also written to the audit log, which the application can never delete from.\n\nScope: `influencers:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "influencers"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The influencer id.",
            "schema": {
              "type": "string",
              "description": "The influencer id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "attest",
                      "withdraw",
                      "refuse"
                    ],
                    "description": "What is being recorded. `attest` says a release is held and lodges it; `withdraw` takes back a consent that was given; `refuse` records that the subject said no."
                  },
                  "statement": {
                    "type": "string",
                    "description": "What was agreed, withdrawn or refused, in your own words: a licence period, a territory, a named campaign. Up to 2,000 characters, because a customer who has to compress that into a tweet writes something useless instead."
                  },
                  "subjectName": {
                    "type": "string",
                    "description": "Who the subject is. Up to 120 characters."
                  },
                  "artefact": {
                    "type": "object",
                    "description": "Required when `action` is `attest`, optional on `withdraw` and `refuse`, and stored on the record whichever action carried it. `{ id, filename, kind }` from POST /influencers/consent-artefacts, after the upload has completed. Sent with a withdrawal it is verified first, so an id whose upload never arrived is a 400 and nothing is recorded; omit the field to withdraw immediately, which always works and is never blocked by a document."
                  }
                },
                "required": [
                  "action",
                  "statement"
                ],
                "additionalProperties": false
              },
              "example": {
                "action": "withdraw",
                "subjectName": "Mara Okonkwo",
                "statement": "The subject withdrew their consent by email on 12 August."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "influencerId": "d076f17e-4529-48a3-b5fe-860bc329c5b4",
                    "consentStatus": "withdrawn",
                    "consentPermitsRender": false,
                    "record": {
                      "id": "7c1d3a90-2f5b-4e08-badc-9a6f24e17c55",
                      "action": "withdraw",
                      "subject": "real_person",
                      "subjectName": "Mara Okonkwo",
                      "statement": "The subject withdrew their consent by email on 12 August.",
                      "artefact": null,
                      "attestedBy": {
                        "type": "api_key",
                        "id": "d81ed241-9404-4a1a-8864-242ecc540416",
                        "label": "CI publisher"
                      },
                      "recordedAt": "2026-08-13T10:02:19Z"
                    },
                    "runningAutomations": [
                      {
                        "id": "4ecde50a-7af3-43b9-b66e-a36422c7e51f",
                        "name": "September daily",
                        "status": "launched"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/influencers/{id}": {
      "get": {
        "operationId": "getInfluencer",
        "summary": "Read one influencer.",
        "description": "The full character record: the voice it speaks with, the accounts it publishes from, and the whole consent history rather than only the current answer. A withdrawal that hid the attestation it withdrew would be a record of the present, and what anyone asks this endpoint is what happened.\n\n`consentPermitsRender` is published beside `consentStatus` because the null case is the surprising one. `null` means unrecorded (the character was created before this control existed), and unrecorded still renders. Only a recorded `withdrawn` or `refused` stops it, so a reader who has to re-derive that from the status alone gets the grandfather rule wrong.\n\n`consentRecords` is append-only and ordered newest first, so a likeness attested, withdrawn and attested again carries all three and `consentRecords[0]` is the decision in force. The artefact is published as a key, a size and a digest and never as a URL: it is a signed release naming a private individual, and an endpoint that handed out a link to it would turn a compliance record into a disclosure. The digest is what makes it checkable: the document in the bucket is the document that was lodged.\n\nThe listing does not carry `consentRecords`. Read this endpoint for the history, and note that recording an attestation is not verification of the subject: nothing here contacts the person in the photograph.\n\nScope: `influencers:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "influencers"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The influencer id.",
            "schema": {
              "type": "string",
              "description": "The influencer id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "d076f17e-4529-48a3-b5fe-860bc329c5b4",
                    "name": "Mara",
                    "imageUrl": "https://cdn.looptake.com/d076f17e-4529-48a3-b5fe-860bc329c5b4/portrait.jpg",
                    "description": "Shop owner turned operations nerd.",
                    "niche": "retail",
                    "gender": "female",
                    "age": 34,
                    "ethnicity": "mixed",
                    "tags": [
                      "retail",
                      "operations"
                    ],
                    "voiceId": "voice_warm_en_3",
                    "status": "ready",
                    "consentStatus": "attested",
                    "consentPermitsRender": true,
                    "previewVideoUrl": "https://cdn.looptake.com/d076f17e-4529-48a3-b5fe-860bc329c5b4/preview.mp4",
                    "creditsSpent": 10,
                    "error": null,
                    "createdAt": "2026-08-12T11:31:12Z",
                    "consentRecords": [
                      {
                        "id": "7c1d3a90-2f5b-4e08-badc-9a6f24e17c55",
                        "action": "attest",
                        "subject": "real_person",
                        "subjectName": "Mara Okonkwo",
                        "statement": "Signed likeness release, worldwide, 12 months, on file.",
                        "artefact": {
                          "key": "invoice/e7293116-fd57-4456-8da0-032f4976145f/91e14cbf-2c80-4b3b-a6bf-776bbde6b54c/consent/3f8a6c21-0d94-4f7a-9c62-51a0b7e4d8f3/release.pdf",
                          "kind": "written_release",
                          "bytes": 184320,
                          "sha256": "9f2c4d7b1a83e60c5518d9f4a7b2e0c36d81f5a94e73b02c6f1d8a45b93e7c20"
                        },
                        "attestedBy": {
                          "type": "api_key",
                          "id": "d81ed241-9404-4a1a-8864-242ecc540416",
                          "label": "CI publisher"
                        },
                        "recordedAt": "2026-08-12T11:31:12Z"
                      }
                    ],
                    "accounts": [
                      {
                        "id": "1c561cb8-4ea0-4fed-84af-bf13a6efe8e9",
                        "platform": "tiktok",
                        "handle": "@mara.stock"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteInfluencer",
        "summary": "Delete a presenter and free its slot.",
        "description": "Removes the presenter and gives back the plan slot it was holding. The cap counts presenters you hold rather than presenters you are using, so a character you have finished with, or one whose subject has withdrawn consent, occupies a paid slot until you delete it. Every video rendered from the presenter is deleted with it. Posts already published are not: they keep their media, their analytics and their place in the calendar. What they lose is the presenter: the link is cleared on every one of them, so `GET /posts?influencerId=` answers nothing for a presenter you have deleted. List first, delete second.\n\nNothing is refunded. The credits bought a preview that rendered, and a delete that gave them back would make create-and-delete a free loop. A preview or a video still rendering when you delete it is the one case that does return credits, and it returns them through the ordinary failure path rather than through this call: the render finds its row gone, fails terminally, and the automatic refund runs.\n\n`videosDeleted` and `consentRecordsDeleted` are counted inside the transaction that removed them, so they are the number of rows that actually went rather than an estimate taken beforehand.\n\n`postsUnlinked` is spelled differently because it is a different thing: those posts are not deleted, and every one of them is still live. What they lose is the name of the presenter they went out as, and this call is the last moment anything can count them. If you need the list rather than the number, take `GET /posts?influencerId=` before you delete.\n\nDeleting a real-person presenter deletes its consent history too, which is how a subject asking to be erased is erased. What survives is an audit entry naming the presenter id, who deleted it, the consent status at the time and these two counts, plus the earlier entry recording that a release was attested and the digest of the document. Neither carries the subject name. The release itself stays in storage under a retention class that refuses deletion.\n\nWorks in an archived workspace. Calls that spend credits are refused there; a call that gives capacity back is not, because a guard on that would trap you in the state you were leaving.\n\nScope: `influencers:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "influencers"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The influencer id.",
            "schema": {
              "type": "string",
              "description": "The influencer id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "d076f17e-4529-48a3-b5fe-860bc329c5b4",
                    "deleted": true,
                    "videosDeleted": 4,
                    "consentRecordsDeleted": 2,
                    "postsUnlinked": 12
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/influencers/{id}/videos": {
      "post": {
        "operationId": "postInfluencerVideos",
        "summary": "Render a talking-head video.",
        "description": "Queues a lip-synced video of the influencer delivering your script, and returns immediately. The cost is 5 credits per second of finished video, so a 20-second read is 100 credits. Poll GET /videos/{id} until the status is terminal.\n\nTerminal statuses: `ready` and `failed`. A failure refunds the reserved credits automatically.\n\nHonours `Idempotency-Key`, because the call spends credits.\n\nScope: `influencers:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts 5 credits per second of video.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "influencers"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The influencer id.",
            "schema": {
              "type": "string",
              "description": "The influencer id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "script": {
                    "type": "string",
                    "description": "What they say, word for word. Paced to `duration` rather than cut to it."
                  },
                  "imageUrl": {
                    "type": "string",
                    "description": "Overrides the influencer’s portrait for this one render.",
                    "format": "uri"
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "ar",
                      "bg",
                      "bn",
                      "cs",
                      "da",
                      "de",
                      "el",
                      "en",
                      "en-GB",
                      "es-419",
                      "es-ES",
                      "et",
                      "fi",
                      "fil",
                      "fr-CA",
                      "fr-FR",
                      "he",
                      "hi",
                      "hr",
                      "hu",
                      "id",
                      "it",
                      "ja",
                      "ko",
                      "lt",
                      "lv",
                      "ms",
                      "nl",
                      "no",
                      "pl",
                      "pt-BR",
                      "pt-PT",
                      "ro",
                      "ru",
                      "sk",
                      "sl",
                      "sv",
                      "ta",
                      "th",
                      "tr",
                      "uk",
                      "ur",
                      "vi",
                      "zh-Hans"
                    ],
                    "description": "BCP 47 language tag for the voice and the captions. One of the 44 languages Looptake generates; a tag outside the list is 400 invalid_input rather than a silent fall back to English.",
                    "default": "en"
                  },
                  "duration": {
                    "type": "integer",
                    "description": "Target length in seconds. Drives the credit cost at 5 per second."
                  },
                  "mode": {
                    "type": "string",
                    "description": "Render mode to produce the video in. Defaults to the influencer’s usual mode."
                  }
                },
                "required": [
                  "script"
                ],
                "additionalProperties": false
              },
              "example": {
                "script": "A stockout does not cost you one sale. It costs you the customer who came for it.",
                "language": "en",
                "duration": 12
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "08363fcd-f524-4126-b68d-fd64f2f9c09e",
                    "influencerId": "d076f17e-4529-48a3-b5fe-860bc329c5b4",
                    "status": "queued",
                    "durationS": 12,
                    "creditsReserved": 60,
                    "statusUrl": "https://looptake.com/api/v1/videos/08363fcd-f524-4126-b68d-fd64f2f9c09e"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getInfluencerVideos",
        "summary": "List an influencer’s videos.",
        "description": "Every video rendered for this character, newest first, with its status and what it cost.\n\nScope: `influencers:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "influencers"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The influencer id.",
            "schema": {
              "type": "string",
              "description": "The influencer id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "One-based page number.",
            "schema": {
              "type": "integer",
              "description": "One-based page number.",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "description": "Items per page.",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "08363fcd-f524-4126-b68d-fd64f2f9c09e",
                      "status": "ready",
                      "language": "en",
                      "durationS": 12,
                      "videoUrl": "https://cdn.looptake.com/08363fcd-f524-4126-b68d-fd64f2f9c09e/video.mp4",
                      "thumbnailUrl": "https://cdn.looptake.com/08363fcd-f524-4126-b68d-fd64f2f9c09e/thumb.jpg",
                      "creditsSpent": 60,
                      "createdAt": "2026-08-12T11:33:40Z"
                    }
                  ],
                  "pagination": {
                    "total": 9,
                    "page": 1,
                    "limit": 1,
                    "pages": 9
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/videos/{id}": {
      "get": {
        "operationId": "getVideo",
        "summary": "Read one video.",
        "description": "Poll this after POST /videos, POST /remix, POST /clips or POST /influencers/{id}/videos. A prompt-first video reports `status` (`queued`, `running`, `ready`, `failed`), `stage`, `progress`, `viralScore` (0-100), `hooks` and `outputs` (content ids and preview URLs); it needs `generations:read`. An influencer video reports `queued`, `processing`, `ready` or `failed` and needs `influencers:read`.\n\nScope: `influencers:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "influencers"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The video id.",
            "schema": {
              "type": "string",
              "description": "The video id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "08363fcd-f524-4126-b68d-fd64f2f9c09e",
                    "influencerId": "d076f17e-4529-48a3-b5fe-860bc329c5b4",
                    "script": "A stockout does not cost you one sale. It costs you the customer who came for it.",
                    "language": "en",
                    "durationS": 12,
                    "mode": "studio",
                    "status": "ready",
                    "videoUrl": "https://cdn.looptake.com/08363fcd-f524-4126-b68d-fd64f2f9c09e/video.mp4",
                    "thumbnailUrl": "https://cdn.looptake.com/08363fcd-f524-4126-b68d-fd64f2f9c09e/thumb.jpg",
                    "creditsSpent": 60
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/assets": {
      "get": {
        "operationId": "getAssets",
        "summary": "List the asset library.",
        "description": "`turboEnabled` marks the clips and images you have offered to the Turbo formats. It is a label on your own library and a filter on this call; nothing in this build draws on the pool automatically yet. `url` is ours once the file has been stored: a short-lived signed link that this call re-signs for you when it has expired, so a URL you read here always works. For an asset we could not store it stays the address you gave.\n\nScope: `assets:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "assets"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Filters by media kind.",
            "schema": {
              "type": "string",
              "enum": [
                "image",
                "video",
                "audio",
                "font"
              ],
              "description": "Filters by media kind."
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Your own grouping, for example `b-roll` or `product`.",
            "schema": {
              "type": "string",
              "description": "Your own grouping, for example `b-roll` or `product`."
            }
          },
          {
            "name": "turboEnabled",
            "in": "query",
            "required": false,
            "description": "Only assets you have offered to the Turbo formats.",
            "schema": {
              "type": "boolean",
              "description": "Only assets you have offered to the Turbo formats."
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "One-based page number.",
            "schema": {
              "type": "integer",
              "description": "One-based page number.",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "description": "Items per page.",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "a7273471-d932-4f9a-958e-79abdd1f3fd7",
                      "kind": "video",
                      "category": "b-roll",
                      "name": "Warehouse aisle, slow pan",
                      "url": "https://cdn.looptake.com/a7273471-d932-4f9a-958e-79abdd1f3fd7/clip.mp4",
                      "bytes": 4812004,
                      "width": 1080,
                      "height": 1920,
                      "durationMs": 9200,
                      "turboEnabled": true
                    }
                  ],
                  "pagination": {
                    "total": 62,
                    "page": 1,
                    "limit": 1,
                    "pages": 62
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postAssets",
        "summary": "Add an asset by URL.",
        "description": "Fetches the file, stores it in our own bucket, and reads its real dimensions and duration out of the file’s own header rather than trusting what you declared. All of that is asynchronous: the 201 carries the row with the URL you gave, and `url`, `width`, `height`, `durationMs` and `bytes` are replaced moments later, so poll `GET /assets` if you need them. Measurements stay `null` for a container this build cannot parse; `null` means \"not established\", never zero. Images and video are stored (MP4, MOV, WebM, PNG, JPEG, GIF, WebP); an `audio` or `font` asset, and any URL that does not answer, keeps the address you gave and is never measured.\n\nScope: `assets:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "assets"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Where to fetch the file from. Must be publicly reachable at the time of the call.",
                    "format": "uri"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "image",
                      "video",
                      "audio",
                      "font"
                    ],
                    "description": "Declared kind. Detected from the response when omitted."
                  },
                  "name": {
                    "type": "string",
                    "description": "Label in the library. Defaults to the filename."
                  },
                  "category": {
                    "type": "string",
                    "description": "Your own grouping."
                  },
                  "turboEnabled": {
                    "type": "boolean",
                    "description": "Offer this file to the Turbo formats. Off by default, so nothing you upload for one campaign appears in a post you did not choose it for. Changeable afterwards with `PATCH /assets/{id}`."
                  }
                },
                "required": [
                  "url"
                ],
                "additionalProperties": false
              },
              "example": {
                "url": "https://cdn.example.com/aisle.mp4",
                "kind": "video",
                "name": "Warehouse aisle, slow pan",
                "category": "b-roll",
                "turboEnabled": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "a7273471-d932-4f9a-958e-79abdd1f3fd7",
                    "kind": "video",
                    "name": "Warehouse aisle, slow pan",
                    "category": "b-roll",
                    "url": "https://cdn.looptake.com/a7273471-d932-4f9a-958e-79abdd1f3fd7/clip.mp4",
                    "bytes": null,
                    "width": null,
                    "height": null,
                    "durationMs": null,
                    "turboEnabled": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/assets/{id}": {
      "patch": {
        "operationId": "patchAsset",
        "summary": "Rename an asset, refile it, or change what Turbo may use.",
        "description": "The three things you can correct after the upload. Every field is optional and a field you leave out is left alone, so renaming a clip does not clear the grouping it is filed under. Send `category: null` to clear the grouping; `name` cannot be cleared, because an asset always has one. Sending an empty body is `invalid_input` rather than a 200 that changed nothing.\n\nScope: `assets:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "assets"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The asset id.",
            "schema": {
              "type": "string",
              "description": "The asset id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The label shown in the library."
                  },
                  "category": {
                    "type": "string",
                    "description": "Your own grouping. Send `null` to clear it."
                  },
                  "turboEnabled": {
                    "type": "boolean",
                    "description": "Whether this file is offered to the Turbo formats."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "name": "Warehouse aisle, slow pan",
                "turboEnabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "a7273471-d932-4f9a-958e-79abdd1f3fd7",
                    "kind": "video",
                    "category": "b-roll",
                    "name": "Warehouse aisle, slow pan",
                    "url": "https://cdn.looptake.com/a7273471-d932-4f9a-958e-79abdd1f3fd7/clip.mp4",
                    "bytes": 4812004,
                    "width": 1080,
                    "height": 1920,
                    "durationMs": 9200,
                    "turboEnabled": false
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteAsset",
        "summary": "Delete an asset and the file behind it.",
        "description": "Removes the library row and the stored file together. `objectsRemoved` is how many objects went with it: `1` for an asset we stored, `0` for one we never could store (an audio file, a font, or a URL that never answered), where the row was only ever a pointer at your own address. Decks that referenced the asset are not rewritten, so a scheduled post naming a file you have deleted will render without it. Deleting twice is a 404.\n\nThis is the one call in this API that removes stored bytes. Everything else keeps what it has written.\n\nScope: `assets:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "assets"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The asset id.",
            "schema": {
              "type": "string",
              "description": "The asset id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "a7273471-d932-4f9a-958e-79abdd1f3fd7",
                    "deleted": true,
                    "objectsRemoved": 1
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/{id}": {
      "get": {
        "operationId": "getContent",
        "summary": "Read render status.",
        "description": "The endpoint every content studio job is polled on. `pending` and `rendering` mean keep polling; `ready` and `failed` are terminal. A failed render refunds its credits without a ticket, and `creditsSpent` returns to zero when it does.\n\nScope: `content:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "content"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The content id.",
            "schema": {
              "type": "string",
              "description": "The content id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "c02119cb-ed7a-40ed-87fe-c18edc1fa1a2",
                    "generationId": "6522d1ec-2c11-4d3a-b078-6f6cfe9f217b",
                    "format": "walloftext",
                    "renderStatus": "ready",
                    "creditsSpent": 0,
                    "media": [
                      {
                        "kind": "video",
                        "url": "https://cdn.looptake.com/c02119cb-ed7a-40ed-87fe-c18edc1fa1a2/video.mp4",
                        "width": 1080,
                        "height": 1920,
                        "durationMs": 8000
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/{id}/download": {
      "get": {
        "operationId": "getContentDownload",
        "summary": "Get a download URL for the finished media.",
        "description": "Returns a short-lived signed URL when the media exists. When it does not, the call starts the render and answers 409 rather than blocking, so a download is never a request that hangs for two minutes. Poll the same URL until it answers 200.\n\nA 409 on a piece that has not started means the render has just been started by this call; on one already rendering it means a worker has it. Retry after a few seconds either way. A 409 on a render that failed says so and is not retried: the credits for it were already returned.\n\nGenerated media is retained indefinitely and nothing expires it on a timer. Archiving a workspace keeps every piece in it. The one operation here that removes a piece is DELETE /influencers/{id}, which takes the videos rendered from that presenter with it; a content piece, a deck and a post render have no delete on this surface.\n\nScope: `content:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "content"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The content id.",
            "schema": {
              "type": "string",
              "description": "The content id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "url": "https://cdn.looptake.com/c02119cb-ed7a-40ed-87fe-c18edc1fa1a2/video.mp4?sig=…",
                    "expiresAt": "2026-08-12T12:35:00Z",
                    "bytes": 3120044,
                    "contentType": "video/mp4"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/character-swap": {
      "post": {
        "operationId": "postContentCharacterSwap",
        "summary": "Replace the person in a video.",
        "description": "Keeps a video that already worked and swaps the presenter, so a proven hook can be re-cut for a new audience without a reshoot. The source video’s timing and framing are preserved exactly.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "videoUrl": {
                    "type": "string",
                    "description": "The source video whose motion and timing are kept.",
                    "format": "uri"
                  },
                  "characterImageUrl": {
                    "type": "string",
                    "description": "The face that replaces the one in the source.",
                    "format": "uri"
                  },
                  "scene": {
                    "type": "string",
                    "description": "Describes the setting to place the new character in."
                  },
                  "keepSourceAudio": {
                    "type": "boolean",
                    "description": "Keeps the original audio track instead of generating a new voice.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "required": [
                  "videoUrl",
                  "characterImageUrl"
                ],
                "additionalProperties": false
              },
              "example": {
                "videoUrl": "https://cdn.example.com/hook.mp4",
                "characterImageUrl": "https://cdn.example.com/mara.jpg",
                "keepSourceAudio": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "a358a75e-6678-42fe-b595-5415fcea62d0",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/a358a75e-6678-42fe-b595-5415fcea62d0"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/green-screen-mobile": {
      "post": {
        "operationId": "postContentGreenScreenMobile",
        "summary": "Present in front of an app screen.",
        "description": "Puts a presenter beside your app screenshot and walks through the flow. This is the format that explains an interaction a screenshot cannot, which is most of what a mobile product has to sell.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "appScreenshot": {
                    "type": "string",
                    "description": "The screen behind the presenter.",
                    "format": "uri"
                  },
                  "presenterImage": {
                    "type": "string",
                    "description": "The person in front of it.",
                    "format": "uri"
                  },
                  "script": {
                    "type": "string",
                    "description": "What the presenter says while the screen is on show."
                  },
                  "duration": {
                    "type": "integer",
                    "description": "Target length in seconds. The script is paced to fit rather than truncated."
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "ar",
                      "bg",
                      "bn",
                      "cs",
                      "da",
                      "de",
                      "el",
                      "en",
                      "en-GB",
                      "es-419",
                      "es-ES",
                      "et",
                      "fi",
                      "fil",
                      "fr-CA",
                      "fr-FR",
                      "he",
                      "hi",
                      "hr",
                      "hu",
                      "id",
                      "it",
                      "ja",
                      "ko",
                      "lt",
                      "lv",
                      "ms",
                      "nl",
                      "no",
                      "pl",
                      "pt-BR",
                      "pt-PT",
                      "ro",
                      "ru",
                      "sk",
                      "sl",
                      "sv",
                      "ta",
                      "th",
                      "tr",
                      "uk",
                      "ur",
                      "vi",
                      "zh-Hans"
                    ],
                    "description": "BCP 47 language tag for the voice and the captions. One of the 44 languages Looptake generates; a tag outside the list is 400 invalid_input rather than a silent fall back to English.",
                    "default": "en"
                  },
                  "captionOverlay": {
                    "type": "boolean",
                    "description": "Burns word-level captions into the video. Leave it on: most short-form is watched muted.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "required": [
                  "appScreenshot",
                  "presenterImage",
                  "script"
                ],
                "additionalProperties": false
              },
              "example": {
                "appScreenshot": "https://cdn.example.com/screen.png",
                "presenterImage": "https://cdn.example.com/mara.jpg",
                "script": "This is the reorder screen. Three inputs, and it tells you the day to order.",
                "duration": 18
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "b1c33352-9456-4902-9c50-20e1f363d387",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/b1c33352-9456-4902-9c50-20e1f363d387"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/clone-video": {
      "post": {
        "operationId": "postContentCloneVideo",
        "summary": "Put your character inside a video that worked.",
        "description": "Takes a video whose movement earned attention and renders your character performing it, keeping the timing that made it work. The output is a new video, not a face pasted onto an old one.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "imageUrl": {
                    "type": "string",
                    "description": "The character to render.",
                    "format": "uri"
                  },
                  "videoUrl": {
                    "type": "string",
                    "description": "The video whose motion is cloned.",
                    "format": "uri"
                  },
                  "videoDuration": {
                    "type": "integer",
                    "description": "Trims the clone to this many seconds from the start."
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "required": [
                  "imageUrl",
                  "videoUrl"
                ],
                "additionalProperties": false
              },
              "example": {
                "imageUrl": "https://cdn.example.com/mara.jpg",
                "videoUrl": "https://cdn.example.com/trend.mp4",
                "videoDuration": 9
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "27afc4b5-41d2-488f-85cf-9f7e9d7a1094",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/27afc4b5-41d2-488f-85cf-9f7e9d7a1094"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/talking-head": {
      "post": {
        "operationId": "postContentTalkingHead",
        "summary": "One presenter, one script, to camera.",
        "description": "A UGC character delivering your script, lip-synced. Leave the appearance fields out and one is chosen from the brand context; fill them in and the same character can be reproduced across a whole campaign.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "script": {
                    "type": "string",
                    "description": "What they say, word for word."
                  },
                  "avatarImageUrl": {
                    "type": "string",
                    "description": "A specific face to use. Omit and the presenter is built from the appearance fields below, or from your brand context when those are empty.",
                    "format": "uri"
                  },
                  "duration": {
                    "type": "integer",
                    "description": "Target length in seconds. The script is paced to fit rather than truncated."
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "ar",
                      "bg",
                      "bn",
                      "cs",
                      "da",
                      "de",
                      "el",
                      "en",
                      "en-GB",
                      "es-419",
                      "es-ES",
                      "et",
                      "fi",
                      "fil",
                      "fr-CA",
                      "fr-FR",
                      "he",
                      "hi",
                      "hr",
                      "hu",
                      "id",
                      "it",
                      "ja",
                      "ko",
                      "lt",
                      "lv",
                      "ms",
                      "nl",
                      "no",
                      "pl",
                      "pt-BR",
                      "pt-PT",
                      "ro",
                      "ru",
                      "sk",
                      "sl",
                      "sv",
                      "ta",
                      "th",
                      "tr",
                      "uk",
                      "ur",
                      "vi",
                      "zh-Hans"
                    ],
                    "description": "BCP 47 language tag for the voice and the captions. One of the 44 languages Looptake generates; a tag outside the list is 400 invalid_input rather than a silent fall back to English.",
                    "default": "en"
                  },
                  "mode": {
                    "type": "string",
                    "description": "Render mode to produce the video in."
                  },
                  "gender": {
                    "type": "string",
                    "description": "Presented gender of the character to choose."
                  },
                  "age": {
                    "type": "integer",
                    "description": "Apparent age of the character to choose."
                  },
                  "ethnicity": {
                    "type": "string",
                    "description": "Apparent ethnicity of the character to choose."
                  },
                  "appearance": {
                    "type": "string",
                    "description": "Free text: clothing, setting, framing."
                  },
                  "referenceImages": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Extra references that keep the character consistent between renders."
                  },
                  "captionOverlay": {
                    "type": "boolean",
                    "description": "Burns word-level captions into the video. Leave it on: most short-form is watched muted.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "required": [
                  "script"
                ],
                "additionalProperties": false
              },
              "example": {
                "script": "Your reorder point is a guess until you know three numbers. Here they are.",
                "duration": 20,
                "language": "en",
                "captionOverlay": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "218dc08b-83e1-4f26-a5e9-b84ce8a0bed0",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/218dc08b-83e1-4f26-a5e9-b84ce8a0bed0"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/talking-head-green-screen": {
      "post": {
        "operationId": "postContentTalkingHeadGreenScreen",
        "summary": "Presenter over your demo footage.",
        "description": "The presenter sits in a corner of the frame and your demo plays behind them, which is the layout that lets a claim and the proof of it share one screen.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "script": {
                    "type": "string",
                    "description": "What the presenter says over the demo."
                  },
                  "avatarImageUrl": {
                    "type": "string",
                    "description": "The presenter’s face.",
                    "format": "uri"
                  },
                  "demoVideoUrl": {
                    "type": "string",
                    "description": "The footage behind them.",
                    "format": "uri"
                  },
                  "demoThumb": {
                    "type": "string",
                    "description": "Cover frame for the demo layer.",
                    "format": "uri"
                  },
                  "avatarPosition": {
                    "type": "string",
                    "description": "Where the presenter sits in the frame, for example `bottom-left`."
                  },
                  "duration": {
                    "type": "integer",
                    "description": "Target length in seconds. The script is paced to fit rather than truncated."
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "ar",
                      "bg",
                      "bn",
                      "cs",
                      "da",
                      "de",
                      "el",
                      "en",
                      "en-GB",
                      "es-419",
                      "es-ES",
                      "et",
                      "fi",
                      "fil",
                      "fr-CA",
                      "fr-FR",
                      "he",
                      "hi",
                      "hr",
                      "hu",
                      "id",
                      "it",
                      "ja",
                      "ko",
                      "lt",
                      "lv",
                      "ms",
                      "nl",
                      "no",
                      "pl",
                      "pt-BR",
                      "pt-PT",
                      "ro",
                      "ru",
                      "sk",
                      "sl",
                      "sv",
                      "ta",
                      "th",
                      "tr",
                      "uk",
                      "ur",
                      "vi",
                      "zh-Hans"
                    ],
                    "description": "BCP 47 language tag for the voice and the captions. One of the 44 languages Looptake generates; a tag outside the list is 400 invalid_input rather than a silent fall back to English.",
                    "default": "en"
                  },
                  "captionOverlay": {
                    "type": "boolean",
                    "description": "Burns word-level captions into the video. Leave it on: most short-form is watched muted.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "required": [
                  "script",
                  "avatarImageUrl",
                  "demoVideoUrl"
                ],
                "additionalProperties": false
              },
              "example": {
                "script": "Watch what happens when stock drops below the reorder point.",
                "avatarImageUrl": "https://cdn.example.com/mara.jpg",
                "demoVideoUrl": "https://cdn.example.com/demo.mp4",
                "avatarPosition": "bottom-left"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "7f0be271-ff4a-4c16-8810-a8910590c564",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/7f0be271-ff4a-4c16-8810-a8910590c564"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/interview": {
      "post": {
        "operationId": "postContentInterview",
        "summary": "A two-shot podcast cut.",
        "description": "The claim arrives as overheard rather than announced, which is why an interview cut carries further than a piece to camera saying the same words.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "script": {
                    "type": "string",
                    "description": "The exchange, as it should be spoken."
                  },
                  "avatarImageUrl": {
                    "type": "string",
                    "description": "The interviewee’s face.",
                    "format": "uri"
                  },
                  "duration": {
                    "type": "integer",
                    "description": "Target length in seconds. The script is paced to fit rather than truncated."
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "ar",
                      "bg",
                      "bn",
                      "cs",
                      "da",
                      "de",
                      "el",
                      "en",
                      "en-GB",
                      "es-419",
                      "es-ES",
                      "et",
                      "fi",
                      "fil",
                      "fr-CA",
                      "fr-FR",
                      "he",
                      "hi",
                      "hr",
                      "hu",
                      "id",
                      "it",
                      "ja",
                      "ko",
                      "lt",
                      "lv",
                      "ms",
                      "nl",
                      "no",
                      "pl",
                      "pt-BR",
                      "pt-PT",
                      "ro",
                      "ru",
                      "sk",
                      "sl",
                      "sv",
                      "ta",
                      "th",
                      "tr",
                      "uk",
                      "ur",
                      "vi",
                      "zh-Hans"
                    ],
                    "description": "BCP 47 language tag for the voice and the captions. One of the 44 languages Looptake generates; a tag outside the list is 400 invalid_input rather than a silent fall back to English.",
                    "default": "en"
                  },
                  "gender": {
                    "type": "string",
                    "description": "Presented gender of the character to choose."
                  },
                  "age": {
                    "type": "integer",
                    "description": "Apparent age of the character to choose."
                  },
                  "ethnicity": {
                    "type": "string",
                    "description": "Apparent ethnicity of the character to choose."
                  },
                  "appearance": {
                    "type": "string",
                    "description": "Free text: clothing, setting, framing."
                  },
                  "captionOverlay": {
                    "type": "boolean",
                    "description": "Burns word-level captions into the video. Leave it on: most short-form is watched muted.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "required": [
                  "script"
                ],
                "additionalProperties": false
              },
              "example": {
                "script": "So what actually goes wrong? You order on a Monday because you always have.",
                "duration": 22
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "20979bf3-5cb8-49c3-a9d9-da3e185bf0bd",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/20979bf3-5cb8-49c3-a9d9-da3e185bf0bd"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/product-spokesperson": {
      "post": {
        "operationId": "postContentProductSpokesperson",
        "summary": "A presenter holding your product.",
        "description": "Generated from one photograph of the product, so the thing in their hands is yours rather than a stand-in that looks like it. The instruction field is where you say what they should do with it.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "script": {
                    "type": "string",
                    "description": "What the spokesperson says."
                  },
                  "spokespersonImage": {
                    "type": "string",
                    "description": "The presenter’s face.",
                    "format": "uri"
                  },
                  "avatarImage": {
                    "type": "string",
                    "description": "An alternative character reference.",
                    "format": "uri"
                  },
                  "productImage": {
                    "type": "string",
                    "description": "One photograph of the product, ideally on a plain ground.",
                    "format": "uri"
                  },
                  "instruction": {
                    "type": "string",
                    "description": "What they do with the product: hold it, open it, point at a detail."
                  },
                  "duration": {
                    "type": "integer",
                    "description": "Target length in seconds. The script is paced to fit rather than truncated."
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "ar",
                      "bg",
                      "bn",
                      "cs",
                      "da",
                      "de",
                      "el",
                      "en",
                      "en-GB",
                      "es-419",
                      "es-ES",
                      "et",
                      "fi",
                      "fil",
                      "fr-CA",
                      "fr-FR",
                      "he",
                      "hi",
                      "hr",
                      "hu",
                      "id",
                      "it",
                      "ja",
                      "ko",
                      "lt",
                      "lv",
                      "ms",
                      "nl",
                      "no",
                      "pl",
                      "pt-BR",
                      "pt-PT",
                      "ro",
                      "ru",
                      "sk",
                      "sl",
                      "sv",
                      "ta",
                      "th",
                      "tr",
                      "uk",
                      "ur",
                      "vi",
                      "zh-Hans"
                    ],
                    "description": "BCP 47 language tag for the voice and the captions. One of the 44 languages Looptake generates; a tag outside the list is 400 invalid_input rather than a silent fall back to English.",
                    "default": "en"
                  },
                  "captionOverlay": {
                    "type": "boolean",
                    "description": "Burns word-level captions into the video. Leave it on: most short-form is watched muted.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "required": [
                  "script"
                ],
                "additionalProperties": false
              },
              "example": {
                "script": "It fits in a shirt pocket and it holds a week of stock counts.",
                "productImage": "https://cdn.example.com/scanner.jpg",
                "instruction": "Hold it up, then turn it to show the screen."
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "a3aa7e95-02af-4bd1-89d4-1fe1e7f0c2a2",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/a3aa7e95-02af-4bd1-89d4-1fe1e7f0c2a2"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/grid-video": {
      "post": {
        "operationId": "postContentGridVideo",
        "summary": "Four variations at once.",
        "description": "A 2x2 grid does the work of four posts in one, and the retention curve tells you which quarter people actually watched.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "prompt": {
                    "type": "string",
                    "description": "Steers the script. Left out, the brand context decides what the piece is about."
                  },
                  "mentionBusiness": {
                    "type": "boolean",
                    "description": "Names the brand in the script. Turn it off for a piece meant to read as editorial.",
                    "default": true
                  },
                  "portrait": {
                    "type": "boolean",
                    "description": "Renders 9:16. Turn it off for a square grid.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "prompt": "Four ways a shelf goes empty",
                "portrait": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "2a3524c5-42fb-493a-bcd9-fabae9e37944",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/2a3524c5-42fb-493a-bcd9-fabae9e37944"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/listicle": {
      "post": {
        "operationId": "postContentListicle",
        "summary": "Numbered points, one at a time.",
        "description": "Paced so each line is read before the next arrives, which is the difference between a list that is watched and a list that is scrolled.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "prompt": {
                    "type": "string",
                    "description": "Steers the script. Left out, the brand context decides what the piece is about."
                  },
                  "mentionBusiness": {
                    "type": "boolean",
                    "description": "Names the brand in the script. Turn it off for a piece meant to read as editorial.",
                    "default": true
                  },
                  "layout": {
                    "type": "string",
                    "description": "How the list is arranged on the frame, for example `stacked` or `card`."
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "prompt": "Five signs you are about to run out of your best seller",
                "layout": "stacked"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "2a862e75-3794-4283-b142-01d05c2722d2",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/2a862e75-3794-4283-b142-01d05c2722d2"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/ask-me-anything": {
      "post": {
        "operationId": "postContentAskMeAnything",
        "summary": "A question card, answered to camera.",
        "description": "Turns the objection you hear on every sales call into a video that handles it before it is asked.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "prompt": {
                    "type": "string",
                    "description": "Steers the script. Left out, the brand context decides what the piece is about."
                  },
                  "mentionBusiness": {
                    "type": "boolean",
                    "description": "Names the brand in the script. Turn it off for a piece meant to read as editorial.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "prompt": "Why not just use a spreadsheet?"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "1f30e99a-fa58-44c3-babe-7d109e9f21df",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/1f30e99a-fa58-44c3-babe-7d109e9f21df"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/ranking": {
      "post": {
        "operationId": "postContentRanking",
        "summary": "A tier list from your category.",
        "description": "Invites the disagreement in the comments that every ranking format earns, and puts your category in front of people who came to argue about it.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "prompt": {
                    "type": "string",
                    "description": "Steers the script. Left out, the brand context decides what the piece is about."
                  },
                  "mentionBusiness": {
                    "type": "boolean",
                    "description": "Names the brand in the script. Turn it off for a piece meant to read as editorial.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "prompt": "Stock-control methods, worst to best"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "5b4c7516-18a4-445e-b3e0-be00fc03de0c",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/5b4c7516-18a4-445e-b3e0-be00fc03de0c"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/split-screen": {
      "post": {
        "operationId": "postContentSplitScreen",
        "summary": "Demo above, retention clip below.",
        "description": "The device that keeps a thumb still long enough for the demo to land. The lower half is not the message; it is the reason the upper half gets watched.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "prompt": {
                    "type": "string",
                    "description": "Steers the script. Left out, the brand context decides what the piece is about."
                  },
                  "caption": {
                    "type": "string",
                    "description": "On-screen text over the demo half."
                  },
                  "mentionBusiness": {
                    "type": "boolean",
                    "description": "Names the brand in the script. Turn it off for a piece meant to read as editorial.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "prompt": "Reorder point, explained in nine seconds",
                "caption": "The number you are guessing"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "7853b5b4-c9ce-46ad-b46b-c4a28f3e3a69",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/7853b5b4-c9ce-46ad-b46b-c4a28f3e3a69"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/fade-in-video": {
      "post": {
        "operationId": "postContentFadeInVideo",
        "summary": "One image, one line, one slow push.",
        "description": "The quietest format in the set, and the one that works when the product is the whole argument.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "prompt": {
                    "type": "string",
                    "description": "Steers the script. Left out, the brand context decides what the piece is about."
                  },
                  "imageUrl": {
                    "type": "string",
                    "description": "The single image. Generated from the brand context when omitted.",
                    "format": "uri"
                  },
                  "mentionBusiness": {
                    "type": "boolean",
                    "description": "Names the brand in the script. Turn it off for a piece meant to read as editorial.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "prompt": "The shelf that never empties",
                "imageUrl": "https://cdn.example.com/shelf.jpg"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "b10ab1bb-64b7-40c4-9173-609bf09fd7a8",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/b10ab1bb-64b7-40c4-9173-609bf09fd7a8"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/content/hook-demo": {
      "post": {
        "operationId": "postContentHookDemo",
        "summary": "A three-second hook stitched onto real footage.",
        "description": "The thing that stops the scroll and the thing that sells are rarely the same clip. This endpoint keeps them separate and joins them at the cut.\n\nScope: `content:write`.\n\nRate limit: 30 requests per 300 seconds.\n\nCosts 10 credits per render.\n\nHonours `Idempotency-Key`. Replaying a key with the same body replays the stored response; a different body returns 409.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "studio"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "hookVideoUrl": {
                    "type": "string",
                    "description": "The opening clip. Three seconds is usually enough.",
                    "format": "uri"
                  },
                  "demoVideoUrl": {
                    "type": "string",
                    "description": "Your real product footage, played after the hook.",
                    "format": "uri"
                  },
                  "caption": {
                    "type": "string",
                    "description": "On-screen text over the hook."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "Steers the script. Left out, the brand context decides what the piece is about."
                  },
                  "hookThumb": {
                    "type": "string",
                    "description": "Cover frame for the hook layer.",
                    "format": "uri"
                  },
                  "mentionBusiness": {
                    "type": "boolean",
                    "description": "Names the brand in the script. Turn it off for a piece meant to read as editorial.",
                    "default": true
                  },
                  "influencerId": {
                    "type": "string",
                    "description": "Attributes the render to an AI influencer, so it can be published from that influencer’s accounts.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  },
                  "name": {
                    "type": "string",
                    "description": "Label for the piece in your library. Defaults to the format name and the creation time."
                  }
                },
                "required": [
                  "hookVideoUrl",
                  "demoVideoUrl"
                ],
                "additionalProperties": false
              },
              "example": {
                "hookVideoUrl": "https://cdn.example.com/hook.mp4",
                "demoVideoUrl": "https://cdn.example.com/demo.mp4",
                "caption": "I found out the expensive way"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "contentId": "6e519ee1-e4a1-4d87-b91b-4a8adc32473f",
                    "renderStatus": "pending",
                    "creditsReserved": 10,
                    "statusUrl": "https://looptake.com/api/v1/content/6e519ee1-e4a1-4d87-b91b-4a8adc32473f"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "insufficient_credits: The team does not have enough credits for the media this call would generate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The resource is not in a state that allows this call, or an idempotency key was replayed with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/analytics/overview": {
      "get": {
        "operationId": "getAnalyticsOverview",
        "summary": "Read performance for a period.",
        "description": "Views, likes, followers and click-through for a period, per platform and in total, from each platform’s own reporting API. The previous period of equal length is returned alongside so a change is a subtraction rather than a second request. Name the period either as one of the three windows or as an arbitrary `from`/`to` range, never both: `?from=2026-01-01&to=2026-03-31` is how you ask \"how did Q1 go\".\n\n`window` and `from`/`to` are two ways of naming one period and sending both is `400 invalid_input`. There is no precedence rule, deliberately: a rule would mean one of the two values you sent did nothing, and the response would confirm the request either way.\n\nA range needs both ends. `from` alone is a filter, not a period, and this endpoint answers with the period *before* the one you asked for, which has no length unless yours does.\n\nA range may span at most 366 days, both ends counted, so a whole leap year is expressible. Longer is `400`; ask twice.\n\n`to` may not be after today. A period that has not finished would be compared against a whole one before it, and every unfinished range would look like a collapse.\n\n`range` is the period as it was actually resolved and queried, as two instants and a day count, so nothing has to re-derive `28d` into dates to draw a chart under these numbers. Both ends are **inclusive**, at microsecond resolution, so `range.from <= publishedAt <= range.to` selects exactly the posts these totals counted. That is why the last microsecond is spelled out rather than rounded: an upper bound of `23:59:59Z` would be about a second narrower than the interval that was queried, and a post published inside that second would be in `totals` and outside the period the same response says it used.\n\n`coverage.since` is when this workspace started existing and `coverage.complete` says whether the period you asked for is entirely inside that. Nothing deletes performance data, so there is no retention horizon to fall off: a period reaching back further than the workspace answers zero because Looptake was not there, which is a different fact from \"nothing was published\" and is the one this field exists to tell you.\n\n`window` is `null` when a range was given. It is never a fourth value: a client switching on it meets an absence rather than a word it has not seen.\n\nScope: `analytics:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "window",
            "in": "query",
            "required": false,
            "description": "How far back to read. Mutually exclusive with `from`/`to`.",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "28d",
                "90d"
              ],
              "description": "How far back to read. Mutually exclusive with `from`/`to`.",
              "default": "28d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "First day of an arbitrary range, inclusive. Send with `to`, and without `window`.",
            "schema": {
              "type": "string",
              "description": "First day of an arbitrary range, inclusive. Send with `to`, and without `window`.",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Last day of an arbitrary range, inclusive. May not be after today.",
            "schema": {
              "type": "string",
              "description": "Last day of an arbitrary range, inclusive. May not be after today.",
              "format": "date"
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Restricts the figures to one platform.",
            "schema": {
              "type": "string",
              "enum": [
                "tiktok",
                "instagram",
                "youtube"
              ],
              "description": "Restricts the figures to one platform."
            }
          },
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "window": null,
                    "range": {
                      "from": "2026-01-01T00:00:00.000000Z",
                      "to": "2026-03-31T23:59:59.999999Z",
                      "days": 90
                    },
                    "coverage": {
                      "since": "2025-11-04T10:12:00Z",
                      "complete": true
                    },
                    "totals": {
                      "views": 184220,
                      "likes": 9104,
                      "comments": 612,
                      "shares": 1488,
                      "saves": 2210,
                      "followers": 1342,
                      "clicks": 806
                    },
                    "previous": {
                      "views": 141980,
                      "likes": 7220,
                      "comments": 501,
                      "shares": 1102,
                      "saves": 1740,
                      "followers": 1109,
                      "clicks": 615
                    },
                    "byPlatform": [
                      {
                        "platform": "tiktok",
                        "views": 121400,
                        "followers": 902
                      },
                      {
                        "platform": "instagram",
                        "views": 41220,
                        "followers": 288
                      },
                      {
                        "platform": "youtube",
                        "views": 21600,
                        "followers": 152
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/analytics/posts": {
      "get": {
        "operationId": "getAnalyticsPosts",
        "summary": "Read per-post performance.",
        "description": "One row per post per platform, sortable, so the question \"which angle worked\" has an answer that is a query rather than an export. `clicks` is populated for posts that carried an attribution link. `signups` is always `null`: a signup happens on your site, and nothing in this schema can see it. Null rather than 0, because \"not measured\" is not \"none\".\n\nScope: `analytics:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Column to order by.",
            "schema": {
              "type": "string",
              "enum": [
                "views",
                "likes",
                "comments",
                "shares",
                "saves",
                "clicks",
                "publishedAt"
              ],
              "description": "Column to order by.",
              "default": "views"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "Sort direction.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "description": "Sort direction.",
              "default": "desc"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound on the publish date.",
            "schema": {
              "type": "string",
              "description": "Inclusive lower bound on the publish date.",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Inclusive upper bound on the publish date.",
            "schema": {
              "type": "string",
              "description": "Inclusive upper bound on the publish date.",
              "format": "date"
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Restricts to one platform.",
            "schema": {
              "type": "string",
              "enum": [
                "tiktok",
                "instagram",
                "youtube"
              ],
              "description": "Restricts to one platform."
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "One-based page number.",
            "schema": {
              "type": "integer",
              "description": "One-based page number.",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "description": "Items per page.",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "postId": "973821cf-5cc7-44f7-8795-e68da31c59ef",
                      "platform": "tiktok",
                      "format": "walloftext",
                      "angle": "The cost of one stockout",
                      "publishedAt": "2026-09-01T15:00:11Z",
                      "views": 48210,
                      "likes": 3110,
                      "comments": 240,
                      "shares": 610,
                      "saves": 880,
                      "clicks": 214,
                      "signups": null
                    }
                  ],
                  "pagination": {
                    "total": 120,
                    "page": 1,
                    "limit": 1,
                    "pages": 120
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/analytics/export": {
      "get": {
        "operationId": "getAnalyticsExport",
        "summary": "Export per-post performance as CSV.",
        "description": "The same rows `GET /analytics/posts` returns for the same filters, as a `text/csv` file with a `Content-Disposition` attachment. One row per post per platform, which is the grain the data has: nothing in Looptake stores a daily history per post, so a per-day column would repeat one cumulative figure or interpolate one, and a per-period file is a single row nobody can pivot. Fields are quoted per RFC 4180 and a value that would begin a spreadsheet formula is prefixed with an apostrophe, so a caption containing a comma, a quote, a newline or a leading `=` arrives intact and inert.\n\nThe file opens as UTF-8 in Excel: it begins with a byte-order mark, so captions in any language render rather than becoming mojibake.\n\nRecords end with CRLF and a caption’s own newline stays a bare LF inside quotes, so splitting on CRLF never cuts a caption in half.\n\n`caption` is here and not in `GET /analytics/posts`: a JSON row hands you `postId` to follow and a spreadsheet row can follow nothing.\n\n`signups` is in `GET /analytics/posts` and not here. It is always `null` there, and a CSV has no null: an empty cell under that heading would read as zero signups.\n\nA filter matching more rows than one export may hold is `400 invalid_input` naming the way out, rather than a truncated file. A CSV has nowhere to put a warning.\n\nScope: `analytics:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.\n\nAnswers `text/csv`, not the success envelope. The body is the file; there is no `data` to unwrap. Errors are still JSON.",
        "tags": [
          "analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Column to order by.",
            "schema": {
              "type": "string",
              "enum": [
                "views",
                "likes",
                "comments",
                "shares",
                "saves",
                "clicks",
                "publishedAt"
              ],
              "description": "Column to order by.",
              "default": "views"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "Sort direction.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "description": "Sort direction.",
              "default": "desc"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound on the publish date.",
            "schema": {
              "type": "string",
              "description": "Inclusive lower bound on the publish date.",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Inclusive upper bound on the publish date.",
            "schema": {
              "type": "string",
              "description": "Inclusive upper bound on the publish date.",
              "format": "date"
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Restricts to one platform.",
            "schema": {
              "type": "string",
              "enum": [
                "tiktok",
                "instagram",
                "youtube"
              ],
              "description": "Restricts to one platform."
            }
          },
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "\"postId\",\"platform\",\"publishedAt\",\"format\",\"angle\",\"caption\",\"views\",\"likes\",\"comments\",\"shares\",\"saves\",\"clicks\"\r\n\"973821cf-5cc7-44f7-8795-e68da31c59ef\",\"tiktok\",\"2026-08-01T15:00:11Z\",\"walloftext\",\"The cost of one stockout\",\"The maths nobody runs, before they run out.\",\"48210\",\"3110\",\"240\",\"610\",\"880\",\"214\"\r\n"
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/analytics/reports": {
      "post": {
        "operationId": "postAnalyticsReports",
        "summary": "Share a performance report by link.",
        "description": "Mints a signed URL that shows one workspace’s numbers for one period to somebody with no Looptake account. The period is resolved at mint time and frozen as two instants: a link shared in August still shows August in October rather than quietly accumulating a quarter of data nobody agreed to send. Name it as a `window` or as a `from`/`to` range, on exactly the rules `GET /analytics/overview` applies, so a period that screen can show is a period this can share. The URL is returned once and cannot be read back, because the token is derived from a secret this deployment stores only sealed.\n\nThe report shows totals, a per-platform breakdown and the ten most-watched posts. It carries no ids of any kind, and no captions from unpublished drafts: every row comes from the published-targets query, so there is no draft in the result set to take one from.\n\nThe brand `angle` is deliberately absent from the shared report. It is the internal reason a piece exists, and it is in the CSV export, which goes to the account holder.\n\n`analytics:read` rather than a scope of its own: the link shows a strict subset of what that scope already reads, so minting one grants nothing a caller did not have.\n\nThe period is read by exactly the function `GET /analytics/overview` reads it with, so every refusal is the same refusal with the same sentence. A period one endpoint took and the other did not would be a `400` a customer meets after choosing what to share.\n\n`window` in the response echoes what was frozen: `28d` for a named window, `2026-04-01..2026-06-30` for a range. It names the period, not the request: a recipient opening the link in October reads the period it was minted for.\n\n`periodStart` and `periodEnd` are the frozen interval in the same **inclusive** form as `range` on GET /analytics/overview, at microsecond resolution. For a range they are the two dates you sent: `from=2026-04-01` comes back as `2026-04-01T00:00:00.000000Z` and not as the last instant of 31 March.\n\nScope: `analytics:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "recipientLabel": {
                    "type": "string",
                    "description": "Who the link is for. Recorded at mint time; it is the only record of who received the numbers."
                  },
                  "window": {
                    "type": "string",
                    "enum": [
                      "7d",
                      "28d",
                      "90d"
                    ],
                    "description": "How far back the frozen period reaches. Mutually exclusive with `from`/`to`.",
                    "default": "28d"
                  },
                  "from": {
                    "type": "string",
                    "description": "First day of an arbitrary frozen period, inclusive. Send with `to`, and without `window`.",
                    "format": "date"
                  },
                  "to": {
                    "type": "string",
                    "description": "Last day of an arbitrary frozen period, inclusive. May not be after today.",
                    "format": "date"
                  },
                  "expiresInDays": {
                    "type": "integer",
                    "description": "How long the link lives. There is no “never”.",
                    "default": 7,
                    "minimum": 1,
                    "maximum": 30
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Scopes the request to one workspace. Defaults to the key’s default workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "required": [
                  "recipientLabel"
                ],
                "additionalProperties": false
              },
              "example": {
                "recipientLabel": "Dana at Acme",
                "window": "28d",
                "expiresInDays": 14
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "b1f0c2d4-8f3a-4a2e-9c17-6b0d5e8a4412",
                    "recipientLabel": "Dana at Acme",
                    "window": "28d",
                    "periodStart": "2026-08-05T09:00:00.000001Z",
                    "periodEnd": "2026-09-02T09:00:00.000000Z",
                    "expiresAt": "2026-09-16T09:00:00Z",
                    "url": "https://looptake.com/report/b1f0c2d4-8f3a-4a2e-9c17-6b0d5e8a4412.1789347600.4f1c9a0e2b7d8351c6a4f0e9b2d7315a8c0e4f61a9d3b7250c8e1f4a6d039b2c"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/analytics/reports/{id}": {
      "delete": {
        "operationId": "deleteAnalyticsReport",
        "summary": "Revoke a shared report link.",
        "description": "Kills the link. Revocation clears the signing secret rather than only stamping a flag, so the copy of the URL already sitting in somebody’s inbox stops resolving immediately. Revoking twice keeps the first instant: the answer to when a report was cut off must not move every time the button is pressed.\n\nAn unknown id, or one belonging to another team, is `404 not_found`.\n\nScope: `analytics:read`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The report id returned when the link was minted.",
            "schema": {
              "type": "string",
              "description": "The report id returned when the link was minted.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "b1f0c2d4-8f3a-4a2e-9c17-6b0d5e8a4412",
                    "revokedAt": "2026-09-03T11:20:04Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/trends": {
      "get": {
        "operationId": "getTrends",
        "summary": "Read the daily trend library.",
        "description": "Sounds, formats, topics and hashtags observed per platform, refreshed daily by the worker. `observedAt` is when the entry was recorded, and it matters: a trend read a week late is a trend you are following rather than riding.\n\nScope: `generations:read`.\n\nRate limit: 10 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "trends"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Restricts to one platform.",
            "schema": {
              "type": "string",
              "enum": [
                "tiktok",
                "instagram",
                "youtube"
              ],
              "description": "Restricts to one platform."
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Restricts to one kind of trend.",
            "schema": {
              "type": "string",
              "enum": [
                "sound",
                "format",
                "topic",
                "hashtag"
              ],
              "description": "Restricts to one kind of trend."
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "One-based page number.",
            "schema": {
              "type": "integer",
              "description": "One-based page number.",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "description": "Items per page.",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "282e5c8b-5e73-4646-b03c-75bc8e0b034c",
                      "platform": "tiktok",
                      "kind": "sound",
                      "title": "Slowed piano loop, 92 bpm",
                      "metric": "Used on 41,000 videos in the last 24 hours",
                      "observedAt": "2026-08-12T05:00:00Z"
                    }
                  ],
                  "pagination": {
                    "total": 80,
                    "page": 1,
                    "limit": 1,
                    "pages": 80
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "operationId": "getWebhooks",
        "summary": "List webhook endpoints and their recent deliveries.",
        "description": "Each endpoint carries its last five delivery outcomes with every attempt behind them: the status your server returned, how long it took, and the transport error when it did not answer at all. This is where to look when events stop arriving. `status` is three-valued: `active`, `paused` (you switched it off) and `disabled` (we did, after a run of consecutive failures, with `disabledReason` saying so). The signing secret is shown once, by the create, and is not readable here or anywhere else.\n\nDelivery is at-least-once. Deduplicate on the `Looptake-Delivery` header, which is the delivery `id` and is stable across every attempt at one event.\n\nScope: `webhooks:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "ae7ede5e-9f5a-42a3-8901-40b74b2130af",
                      "url": "https://northwind.example/hooks/looptake",
                      "events": [
                        "post.published",
                        "post.failed"
                      ],
                      "workspaceId": null,
                      "secretLast4": null,
                      "status": "active",
                      "failureCount": 0,
                      "disabledAt": null,
                      "disabledReason": null,
                      "lastDeliveryAt": "2026-08-13T09:00:12Z",
                      "createdAt": "2026-07-20T08:00:00Z",
                      "deliveries": [
                        {
                          "id": "5b0d6f0a-6e0a-4a3e-8f2f-0d7a2d0e9b11",
                          "event": "post.published",
                          "status": "delivered",
                          "attempts": 2,
                          "responseStatus": 200,
                          "error": null,
                          "nextAttemptAt": null,
                          "deliveredAt": "2026-08-13T09:00:12Z",
                          "createdAt": "2026-08-13T09:00:00Z",
                          "attemptLog": [
                            {
                              "at": "2026-08-13T09:00:00Z",
                              "status": 503,
                              "latencyMs": 87,
                              "error": "the endpoint answered 503"
                            },
                            {
                              "at": "2026-08-13T09:00:12Z",
                              "status": 200,
                              "latencyMs": 41,
                              "error": null
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postWebhooks",
        "summary": "Create a webhook endpoint.",
        "description": "Four events are available: `post.published`, `post.failed`, `content.ready` and `credits.low`. The response carries the signing secret once. Every delivery is signed with it as `Looptake-Signature: t=<unix>,v1=<hex>`, an HMAC-SHA-256 over `<t>.<raw body>`. The timestamp is inside the signed message, so a captured delivery cannot be replayed with a fresh one. The verification recipe is on /developers/docs/webhooks.\n\nThe `secret` field appears in this response only. It cannot be read back afterwards.\n\nA second subscription for the same URL is a 409, not a duplicate: a customer who clicks create twice gets one endpoint rather than two copies of every event.\n\nScope: `webhooks:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Where deliveries are posted. Must be HTTPS.",
                    "format": "uri"
                  },
                  "events": {
                    "type": "string",
                    "enum": [
                      "post.published",
                      "post.failed",
                      "content.ready",
                      "credits.low"
                    ],
                    "description": "Which events to receive. At least one."
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Restricts deliveries to one workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "required": [
                  "url",
                  "events"
                ],
                "additionalProperties": false
              },
              "example": {
                "url": "https://northwind.example/hooks/looptake",
                "events": [
                  "post.published",
                  "post.failed"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "ae7ede5e-9f5a-42a3-8901-40b74b2130af",
                    "url": "https://northwind.example/hooks/looptake",
                    "events": [
                      "post.published",
                      "post.failed"
                    ],
                    "secret": "whsec_9f13c0a8b24e7d5162af38cc90b1e774",
                    "status": "active"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/{id}": {
      "patch": {
        "operationId": "patchWebhook",
        "summary": "Correct an endpoint, or switch it back on.",
        "description": "Every field is optional and a field you leave out is left alone. `events` replaces the list rather than merging into it, because “stop sending me post.failed” has no other spelling; an empty array is `invalid_input`, not “every event”. `active: false` pauses deliveries and keeps everything else. `active: true` resumes, and it is the only way back from `disabled`, which we set ourselves after a run of consecutive failures: a disabled endpoint is sent nothing, so it can never succeed its way out. Resuming clears `disabledAt`, `disabledReason` and `failureCount` together. There is no `workspaceId` here: which workspace an endpoint receives is what its delivery history is a history of, so that is a different subscription rather than an edit.\n\nThe signing secret is not re-minted by an edit, and is still not readable: the endpoint keeps the secret it was created with, so nothing at your end has to be redeployed.\n\nSending an empty body is `invalid_input` rather than a 200 that changed nothing.\n\nMoving an endpoint onto a URL the team already subscribes is a `409`, the same answer the create gives.\n\nScope: `webhooks:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The webhook id.",
            "schema": {
              "type": "string",
              "description": "The webhook id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Where deliveries are posted. Must be HTTPS.",
                    "format": "uri"
                  },
                  "events": {
                    "type": "string",
                    "enum": [
                      "post.published",
                      "post.failed",
                      "content.ready",
                      "credits.low"
                    ],
                    "description": "The complete new event list. At least one; replaces what is there."
                  },
                  "active": {
                    "type": "boolean",
                    "description": "Whether to deliver to this endpoint. `true` also clears a disablement and its failure count."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "events": [
                  "post.published"
                ],
                "active": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "ae7ede5e-9f5a-42a3-8901-40b74b2130af",
                    "url": "https://northwind.example/hooks/looptake",
                    "events": [
                      "post.published"
                    ],
                    "workspaceId": null,
                    "secretLast4": null,
                    "status": "active",
                    "failureCount": 0,
                    "disabledAt": null,
                    "disabledReason": null,
                    "lastDeliveryAt": "2026-08-13T09:00:12Z",
                    "createdAt": "2026-07-20T08:00:00Z",
                    "deliveries": []
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "summary": "Delete a webhook endpoint.",
        "description": "Stops deliveries immediately. Anything already in flight is not recalled. The delivery history goes with the row, and the signing secret cannot be recovered, so if you only want deliveries to stop, `PATCH` with `active: false` instead.\n\nScope: `webhooks:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The webhook id.",
            "schema": {
              "type": "string",
              "description": "The webhook id.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "ae7ede5e-9f5a-42a3-8901-40b74b2130af",
                    "deleted": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/brand/analyze": {
      "post": {
        "operationId": "postBrandAnalyze",
        "summary": "Re-derive the brand context.",
        "description": "Crawls the site again and rebuilds the identity, audience, tone, value propositions, angles, keywords, competitors and palette that every generation is written from. Run it after a repositioning or a pricing change; every batch generated afterwards is written from the new reading.\n\nPoll GET /workspaces/{id} until `brandContext.refreshedAt` moves past the time of this call.\n\nScope: `workspaces:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.\n\nReturns 202. The work is accepted and must be polled at the returned `statusUrl`.",
        "tags": [
          "brand"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "The site to read. Defaults to the workspace’s own `websiteUrl`.",
                    "format": "uri"
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Which workspace to rebuild. Defaults to the key’s default workspace.",
                    "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "url": "https://northwind.example"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll the status URL.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "workspaceId": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                    "status": "analyzing",
                    "statusUrl": "https://looptake.com/api/v1/workspaces/91e14cbf-2c80-4b3b-a6bf-776bbde6b54c"
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/brand": {
      "delete": {
        "operationId": "deleteBrand",
        "summary": "Throw the derived brand context away.",
        "description": "The undo for a first crawl that read the wrong site. Re-analysing already replaces a context, so this is for the case re-analysing cannot reach: a crawl that fails (a client-rendered page, a login wall, a domain that has moved) writes nothing, and the wrong reading stays and goes on being what every suggestion, automation batch and caption is written from. A context that is wrong is worse than none; with none, the dashboard asks for a site again. Deleting twice is a 404.\n\nNothing else is removed: the workspace, its accounts, its posts and everything it has published are untouched.\n\nScope: `workspaces:write`.\n\nRate limit: 600 requests per 300 seconds.\n\nCosts no credits.",
        "tags": [
          "brand"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "workspaceId",
            "in": "query",
            "required": false,
            "description": "Which workspace to clear. Defaults to the key’s default workspace.",
            "schema": {
              "type": "string",
              "description": "Which workspace to clear. Defaults to the key’s default workspace.",
              "pattern": "^[a-z]+_[a-zA-Z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the window."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the window."
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds at which the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "workspaceId": "91e14cbf-2c80-4b3b-a6bf-776bbde6b54c",
                    "deleted": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_input: The request body or query failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized: No key, an unreadable key, or a key that is no longer valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "forbidden_scope: The key is valid but does not carry the scope this endpoint requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "not_found: No resource with that id inside the team the key belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "usage_limit_reached: A plan quota is exhausted: saved posts, automations, content pieces, connected accounts or workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error: Something failed on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key created at https://looptake.com/app/settings/developers. Shown once; stored as a SHA-256 digest."
      },
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "The same key, for clients that cannot set an Authorization header."
      }
    },
    "schemas": {
      "SuccessEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "description": "The payload. Its shape is documented per endpoint."
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "object",
            "required": [
              "type",
              "message"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "invalid_input",
                  "unauthorized",
                  "forbidden_scope",
                  "not_found",
                  "conflict",
                  "insufficient_credits",
                  "usage_limit_reached",
                  "rate_limited",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "Human readable. Never branch on this; branch on `type`."
              }
            }
          }
        }
      },
      "Pagination": {
        "type": "object",
        "required": [
          "total",
          "page",
          "limit",
          "pages"
        ],
        "properties": {
          "total": {
            "type": "integer"
          },
          "page": {
            "type": "integer",
            "minimum": 1
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "pages": {
            "type": "integer"
          }
        }
      }
    }
  },
  "x-scopes": [
    {
      "name": "generations:read",
      "grants": "List suggestions, read one in full, and read the trend library."
    },
    {
      "name": "generations:write",
      "grants": "Generate suggestions, accept one into content, and create A/B hook variants."
    },
    {
      "name": "posts:read",
      "grants": "List posts, read one with its per-platform results, and read the calendar."
    },
    {
      "name": "posts:write",
      "grants": "Create posts, schedule them, and cancel one that has not published yet."
    },
    {
      "name": "posts:approve",
      "grants": "Approve or reject a post that is waiting for review. Deliberately not implied by posts:write, and held only by a signed-in reviewer: an API key cannot be minted with it, because an approval has to name the person who made it."
    },
    {
      "name": "automations:read",
      "grants": "List automations, read one, and list the posts an automation produced."
    },
    {
      "name": "automations:write",
      "grants": "Create, edit, generate, extend, launch, pause, resume and cancel automations."
    },
    {
      "name": "influencers:read",
      "grants": "List AI influencers, read one, and read their rendered videos."
    },
    {
      "name": "influencers:write",
      "grants": "Create AI influencers and render talking-head videos for them. Both spend credits."
    },
    {
      "name": "assets:read",
      "grants": "List the workspace asset library: clips, images, audio and fonts."
    },
    {
      "name": "assets:write",
      "grants": "Add an asset to the library by URL."
    },
    {
      "name": "content:write",
      "grants": "Start a content studio render, read its status, and download the finished media."
    },
    {
      "name": "accounts:read",
      "grants": "List connected TikTok, Instagram and YouTube accounts and their connection status."
    },
    {
      "name": "credits:read",
      "grants": "Read the credit balance and the ledger, including refunds."
    },
    {
      "name": "workspaces:read",
      "grants": "List workspaces and read one with its brand context."
    },
    {
      "name": "workspaces:write",
      "grants": "Create a workspace and re-derive its brand context from a URL."
    },
    {
      "name": "analytics:read",
      "grants": "Read the performance overview and per-post analytics."
    },
    {
      "name": "webhooks:write",
      "grants": "List, create, edit and delete webhook endpoints. There is no separate read scope."
    }
  ],
  "x-rate-limits": [
    {
      "group": "generations",
      "limit": 10,
      "windowSeconds": 300
    },
    {
      "group": "posts",
      "limit": 60,
      "windowSeconds": 300
    },
    {
      "group": "studio",
      "limit": 30,
      "windowSeconds": 300
    },
    {
      "group": "default",
      "limit": 600,
      "windowSeconds": 300
    }
  ]
}