openapi: 3.1.0
info:
  title: Obelisk Partner API
  version: 1.0.0
  license:
    name: Proprietary
  description: |-
    # Obelisk Partner API

    Public, keys-only surface at `/partner/v1` for competitive ad research, AI image and
    video generation, and TikTok-first publishing. One bearer key, one response envelope,
    credits metered per request. This spec documents the live Partner API only — it is not
    the legacy `app/server/openapi.yaml`.

    Common use cases:

    - Research what is running in the market: search Obelisk's curated ad index, the Meta
      Ad Library, the Google Ads Transparency Center, and public social profiles.
    - Generate creative from a prompt or a reference asset, poll the job, download the result.
    - Curate media into collections and publish a TikTok draft for a human to review and post.

    What the Partner API is not:

    - **Not an ads manager.** It reads public ad libraries for research. It does not buy
      placements, manage spend, or touch an ad account.
    - **Not a general media host.** Storage is for assets moving through generation and
      publishing, not a CDN or an asset library for unrelated content.
    - **Not an agent.** It provides the endpoints; bring your own orchestration — or use the
      Claude Code skill and MCP server described below.

    ## Base URL

    ```
    https://api.obeliskapp.io
    ```

    Local stack: `http://127.0.0.1:8080` (or `$BACKEND_PORT`). Every path below is relative
    to the host and begins with `/partner/v1`.

    ## Authentication

    All requests require a bearer token:

    ```
    Authorization: Bearer osk_live_...
    ```

    **Create a key** in the dashboard at `/settings/api-keys` (Clerk session). The secret is
    shown once, in the form `osk_live_<public_id>.<secret>` — example shape
    `osk_live_x1y2z3abcdef.` plus a 32-character secret. There is no key-mint path on
    `/partner/v1`, and Clerk JWTs are rejected here with `401 unauthorized`.

    ### Keyless x402 discovery (flag-gated)

    A dedicated kill switch `X402_KEYLESS_ENABLED` (default **false**, ships dark until
    Issue 8) plus the x402 rail (`X402_ENABLED` and a live CDP facilitator) admits
    **missing** `Authorization` on the 13 discovery GET operations marked
    `x-keyless-x402: true`. The caller pays USDC per request (principal = verified
    payer address). A present `Authorization` that is not a valid `osk_live_` key —
    including Clerk JWTs — is still `401 unauthorized` and is never treated as keyless.
    Scrapers, assets, generation, publishing, account, and ping stay keys-only.

    Anonymous compliance gate: the CDP facilitator screens **payer and recipient** on
    every Verify (OFAC / KYT). `KYT_RISK_DETECTED` and `REQUEST_BLOCKED_BY_LOCATION`
    map to HTTP `403`. There is no second screening client.

    Security on keyless routes remains bearer **or** an x402 payment (`PAYMENT-SIGNATURE`
    after a `402 payment_required` challenge). Do not treat all of `/partner/v1` as keyless.

    ### Scopes

    A key carries a subset of five scopes. Every operation below declares the one it needs as
    `x-required-scope`:

    | Scope | Grants | Operations |
    |---|---|---:|
    | `account` | Key identity and tenant snapshot | 2 |
    | `discovery` | Ad spy, Ad Library, and scraper reads | 16 |
    | `assets` | Media, collections, brands, products | 16 |
    | `generation` | Models, jobs, slideshows | 9 |
    | `publishing` | Connected accounts, posts, automations | 14 |

    **Default dashboard keys omit `publishing`.** A key minted with the default set
    (`account`, `discovery`, `assets`, `generation`) returns `403 forbidden_scope` on
    anything under Accounts, Posts, or Automations — mint a key with `publishing` to post.
    A `403` with `error_code: "forbidden_scope"` always means the key lacks the scope, never
    that the resource is missing; cross-tenant reads return `404 not_found` instead.

    ## Quickstart

    1. **Create a key** at `/settings/api-keys` and export it as `OBELISK_API_KEY`.
    2. `GET /partner/v1/ping` — confirms the key. `message` is `pong`; `data` carries
       `tenant_id`, `key_public_id`, and `scopes`.
    3. `GET /partner/v1/spy/ads?limit=1` — first discovery call (1 credit on HTTP 2xx).
    4. `POST /partner/v1/jobs` with `{type, model, prompt, external_id}`, then poll
       `GET /partner/v1/jobs/{id}` every 5 seconds (up to 24 times, the same cadence as
       `partner_smoke.sh --wait`) until `status` is `completed`, `failed`, or `cancelled`.
    5. `GET /partner/v1/accounts` — requires `publishing`; see Scopes above.
    6. `POST /partner/v1/posts` — a TikTok draft: `tiktok.post_mode: MEDIA_UPLOAD`,
       `tiktok.privacy_level: SELF_ONLY`, `media.type: slideshow`.

    Worked, copy-pasteable versions of steps 3–6 are in **Agent quickstarts** at the end of
    this page.

    ## Resources

    Fifty-eight operations in five groups.

    | Group | Covers |
    |---|---|
    | **Account** | `/ping`, `/account`, `/payments/{tx_id}/receipt` — key identity, tenant, credits, tier, signed x402 receipt |
    | **Discovery** | `/spy/*` curated index · `/facebook-ads/*` Meta Ad Library · `/google-ads/*` Transparency Center · `/scraper/*` public Instagram, TikTok, X profiles |
    | **Assets** | `/media/*` upload, import, download · `/collections/*` · read-only `/brands`, `/products` |
    | **Generation** | `/models` · `/jobs/*` async image and video · `/slideshows/*` multi-slide and per-slide export |
    | **Publishing** | `/accounts` connected socials · `/posts/*` · `/automations/*` recurring rules and run history |

    Ownership: media, collections, brands, products, jobs, slideshows, posts, and automations
    are all tenant-scoped. A read for another tenant's id is `404 not_found`, never `403`.

    ## Credits & tiers

    | Tier | General req/min | Scraper req/min | Concurrent jobs | Active automations |
    |---|---:|---:|---:|---:|
    | Starter | 60 | 10 | 3 | 2 |
    | Pro | 300 | 30 | 10 | 10 |
    | Enterprise | unlimited (`-1`) | unlimited (`-1`) | unlimited (`-1`) | unlimited (`-1`) |

    Metering (live code):

    - Discovery reads cost **1** credit per request. `GET /facebook-ads/platforms` is free.
    - Scraper reads (`/partner/v1/scraper/*`) cost **2**.
    - `POST /jobs` cost is **variable** (`CalculateJobCreditCost`) — reserved at create and refunded on cancel/fail on the **credits** rail. On the **x402** rail the same quote is charged at acceptance in USDC (exact; not an estimate). Settlements are irreversible — a later job failure is not auto-refunded. Human remediation is the AP-8 runbook. `upto` ceiling-settle is a named follow-up only if runtime-variable pricing appears.
    - `POST /slideshows/generate` costs **10**. Slide export costs **1**.
    - `POST /posts` costs **1 per targeted account**.
    - Own-data assets, `/account`, `/ping`, `/payments/{tx_id}/receipt`, `/models`, automations CRUD + `/run`, and list/get/delete of jobs, posts, slideshows, media, collections, brands, and products are **free**. Executor charges generation + publish credits later.

    `X-Credit-Cost` and `X-Credits-Remaining` appear only on charged 2xx responses. They are omitted on free routes and on idempotent replay (same `external_id`, HTTP 200, `data.duplicate: true`).

    ## Errors

    Success: `{ok: true, code, message, data}`. Failure: `{ok: false, code, message, error_code, issues?, request_id}`.

    Branch on `error_code`, not HTTP status alone:

    - `402` is `subscription_required` **or** `insufficient_credits` **or** `payment_required` **or** `spend_limit_exceeded` (disambiguate by `error_code`. `payment_required` carries a `PAYMENT-REQUIRED` header; `spend_limit_exceeded` does **not** — agents stop, a human must raise the key's limit)
    - `429` is `rate_limited` or `concurrent_limit`
    - `422` is usually `invalid_payload` (also `platform_not_supported`)
    - `400` is `validation_failed` / `unknown_accounts` / `media_unreachable` / `invalid_since` / `invalid_until` / `too_many_post_ids` / `bad_request`
    - `403` is usually `forbidden_scope` / `automation_limit`; `payment_required` is 403 only for `kyt_risk_detected` / `request_blocked_by_location` (`issues.reason`)

    Cross-tenant reads are `404 not_found`, never `403`. `500 internal_error` never leaks internals. `502 upstream_error` message is the constant `"upstream provider error"`.

    Dual-rail: subscribers stay on Polar credits (never see x402). Unsubscribed and keyless callers use **Pay-per-call (x402)** — see that section below. `402` without a `PAYMENT-REQUIRED` header is either `spend_limit_exceeded` or a facilitator outage (`issues.reason: facilitator_unavailable`); do not pay those.

    ## Rate limits & retries

    Honor `Retry-After` on `429 rate_limited`. Successful key + subscription requests also receive:

    - `X-RateLimit-Limit`
    - `X-RateLimit-Remaining`
    - `X-RateLimit-Reset` (unix **seconds**, not milliseconds)

    Two buckets: **general** (default) and **scraper** (`/partner/v1/scraper/*`). The limiter is per Cloud Run instance — counts are an approximation under scale-out, not a global Redis limit.

    ## Idempotency

    `external_id` is supported on `POST /jobs` and `POST /posts` only.

    - Same tenant + same `external_id` + same fingerprint → HTTP **200** with `data.duplicate: true` and no second charge.
    - Same `external_id` + different payload → **409** `external_id_conflict`.
    - New create: jobs **202** (`duplicate: false`), posts **201** (`duplicate: false`).

    Recommend agent patterns like `{agent_run_id}:{purpose}`. `external_id` is immutable after create. v1 post edit is DELETE + POST with a **new** `external_id`.

    ## Pay-per-call (x402)

    HTTP 402 native USDC payments (x402 v2) for callers **without** an active Polar
    subscription — including wallet-enabled agents with no Obelisk key on the
    keyless discovery pilot. Subscribers never see x402: Polar credits,
    `Metered` / `MeteredCost`, and `X-Credit-Cost` / `X-Credits-Remaining` are
    unchanged. Two rails, one Partner surface.

    **Who it is for.** Unsubscribed `osk_live_` keys, and (when keyless is on)
    anonymous agents paying from a wallet. Generation, publishing, scrapers,
    assets, account, ping, and receipts stay keys-only.

    **Headers (REST)** and **MCP `_meta` twins:**

    | REST header | MCP `_meta` key | Direction |
    |---|---|---|
    | `PAYMENT-REQUIRED` | `_meta["x402/payment-required"]` | Server → client (402 challenge, base64 JSON on REST) |
    | `PAYMENT-SIGNATURE` | `_meta["x402/payment"]` | Client → server (signed payload) |
    | `PAYMENT-RESPONSE` | `_meta["x402/payment-response"]` | Server → client (settlement + optional `extensions.receipt`) |

    Every challenge advertises the `payment-identifier` extension. Replay the
    same identifier on the same operation (D10) to re-serve a settled 2xx
    **without a second charge**. A 402 with **no** `PAYMENT-REQUIRED` header
    (facilitator outage, `issues.reason: facilitator_unavailable`, or
    `spend_limit_exceeded`) is **not payable**.

    **Pricing.** USD per call = credit cost × `X402_USD_CENTS_PER_CREDIT`
    (live default **1¢ per credit**, floor **1¢**). Facilitator fees are
    absorbed, not added to the quote. Free routes stay $0.

    | Method | Path | Credits | USD (default) | Keyless | Bazaar |
    |---|---|---:|---:|---|---|
    | GET | `/partner/v1/spy/ads` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/spy/ads/:id` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/spy/brands/search` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/spy/brands` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/spy/brands/:id` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/facebook-ads/ads/search` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/facebook-ads/ads/:archive_id/media` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/facebook-ads/ads/:archive_id` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/facebook-ads/companies/search` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/facebook-ads/companies/:page_id/ads` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/facebook-ads/platforms` | 0 | $0 | yes | no (free; no 402) |
    | GET | `/partner/v1/google-ads/companies/:domain/ads` | 1 | $0.01 | yes | yes |
    | GET | `/partner/v1/google-ads/advertisers/:advertiser_id/ads` | 1 | $0.01 | yes | yes |

    MCP keyless priced tools (same cents): `search_ads`, `get_ad`, `search_brands`,
    `get_brand`, `search_facebook_ads`, `search_facebook_companies`,
    `get_facebook_company_ads`, `get_google_ads`. `list_models` is free.

    **Networks.** Production: Base mainnet `eip155:8453`, USDC
    `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. Testnet: Base Sepolia
    `eip155:84532`. Scheme `exact`, asset USDC.

    **Refunds.** Settlements are **irreversible**. There are no chargebacks.
    Contact support for a reverse USDC transfer or a support-mediated re-run
    of a failed async job after settlement.

    **Receipts.** On a settled 2xx, verify `extensions.receipt` (ES256) against
    `https://obeliskapp.io/developers/receipt-public-key.pem`. Keyless buyers
    get the JWT **inline only**. `GET /partner/v1/payments/{tx_id}/receipt` is
    tenant-scoped (`404 not_found`, never `403`).

    Wire notes: no signature → 402 `payment_required` plus `PAYMENT-REQUIRED`.
    Client retries with `PAYMENT-SIGNATURE`. Success sets `PAYMENT-RESPONSE`
    and `X-Payment-Amount-Usd-Cents`. KYT / geo-block → HTTP 403, same
    `error_code: payment_required`, reason in `issues.reason`. Handler non-2xx:
    no settle. Settle timeout → `timeout_unresolved` (body still served). Genuine
    hard settle failure still serves the 2xx (authorization is single-use).
    Duplicate/nonce-used settle → 402 `issues.reason: duplicate_settlement`,
    body **not** served (`error_code` stays `payment_required`). Replay the
    original `payment-identifier` (D10) to recover a lost 2xx; do not sign a
    new payment.

    ## Agent skill
    
    Download the Claude Code skill (`skills/obelisk-partner/` in the repo, or the
    `obelisk-partner.zip` release artifact) and install it as
    `~/.claude/skills/obelisk-partner/` or `.claude/skills/obelisk-partner/`.
    
    The skill teaches discover → generate → curate → TikTok draft against
    `/partner/v1`. Auth is `OBELISK_API_KEY` (never inline). First call every
    session is `GET /partner/v1/account`.
    
    Production host for the skill is `https://api.obeliskapp.io`. Local stack:
    `OBELISK_API_BASE=http://127.0.0.1:${BACKEND_PORT:-8080}`. Default dashboard
    keys omit `publishing` — mint a key with that scope to call accounts/posts.
    Every publish example is a TikTok draft (`MEDIA_UPLOAD` + `SELF_ONLY`).
    
    Source of truth for the three walkthroughs below:
    `skills/obelisk-partner/quickstarts/`.
    
    ## MCP
    
    The Partner API is also exposed as MCP tools (same 43 operations, scopes,
    and credits). `/mcp` is **not** a `/partner/v1` path.
    
    - **Hosted Streamable HTTP:** `GET|POST|DELETE /mcp` with
      `Authorization: Bearer $OBELISK_API_KEY` (keys-only + active
      subscription). HTTP 401/402 use the partner envelope before MCP framing.
    - **Stdio:** `obelisk mcp` on the existing CLI binary. Set
      `OBELISK_API_KEY` and optionally `OBELISK_API_BASE` (default
      `https://api.obeliskapp.io`). The process proxies newline-delimited
      JSON-RPC to `{base}/mcp`. See `obelisk mcp --help` for Claude Code and
      Claude Desktop setup.
    
    Prefer MCP when the client supports it. The skill below still teaches REST.

    ## CLI

    The first-party `obelisk` CLI (`app/server/cmd/obelisk-cli`, brew / npm /
    `cli/v*` binaries) is a wallet-enabled x402 v2 **buyer** for REST.

    Install: see `app/server/cmd/obelisk-cli/README.md`. Config lives at
    `~/.config/obelisk/config.yaml` (0700/0600). `OBELISK_API_KEY` and
    `OBELISK_WALLET_KEY` win over the file.

    ```bash
    obelisk wallet import --key-stdin --max-usd-cents 1 --network eip155:84532
    obelisk ads search --limit 1 --pay
    ```

    Auto-pay requires **both** a per-call cap (`wallet.max_usd_cents_per_call`,
    no default) **and** opt-in (`--pay` or `wallet.autopay: true`). The private
    key is never printed. `wallet status` shows the checksummed address only.

    `obelisk mcp` is a stdio passthrough and does **not** auto-sign
    `_meta["x402/payment"]`. MCP clients attach payment themselves.

    ## Agent quickstarts
    
    ### Your first discovery call
    
    A complete first session: verify the key, then spend **1** credit on a single spy-ad page.
    
    #### What you need
    
    - `OBELISK_API_KEY` — `osk_live_<public_id>.<secret>` from `/settings/api-keys`. Never echo it. Shape (synthetic): `osk_live_x1y2z3abcdef.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`.
    - `OBELISK_API_BASE` — default production `https://api.obeliskapp.io`. Local stack: `http://127.0.0.1:${BACKEND_PORT:-8080}`.
    - Scope `account` (session start) and `discovery` (spy / Facebook). Both are in `DefaultScopes`.
    - At least **1** available credit.
    
    ```bash
    export OBELISK_API_KEY
    export OBELISK_API_BASE="${OBELISK_API_BASE:-https://api.obeliskapp.io}"
    ```
    
    #### 1. Verify auth
    
    First command of every session.
    
    ```bash
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      "${OBELISK_API_BASE}/partner/v1/account" | jq .
    ```
    
    HTTP **200**. Free (no `X-Credit-Cost`). Expected envelope (`key.public_id` only — the secret is never in the response):
    
    ```json
    {
      "ok": true,
      "code": 200,
      "message": "Account retrieved",
      "data": {
        "tenant": {
          "id": "ten_01hxyzexample0001",
          "name": "Acme Studio"
        },
        "subscription": {
          "active": true,
          "tier": "starter",
          "product_id": "prod_starter"
        },
        "credits": {
          "total_purchased": 1000,
          "total_consumed": 42,
          "available": 958
        },
        "key": {
          "public_id": "x1y2z3abcdef",
          "scopes": ["discovery", "generation", "assets", "account"]
        }
      }
    }
    ```
    
    Read `data.key.scopes`, `data.subscription.active`, and `data.credits.available`. If `available < 1`, stop — do not call spy.
    
    #### 2. State the cost
    
    Next call costs **1** credit. Say so: "this spy search costs 1 credit; you have N available."
    
    #### 3. Spy ads (`limit=1`)
    
    ```bash
    curl -sS \
      -D - \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      "${OBELISK_API_BASE}/partner/v1/spy/ads?limit=1" | jq .
    ```
    
    HTTP **200**. Header `X-Credit-Cost: 1` on 2xx. Page keys `items` / `total` / `limit` / `offset`; `limit` is `1`.
    
    ```json
    {
      "ok": true,
      "code": 200,
      "message": "Ads retrieved",
      "data": {
        "items": [
          {
            "id": "ad_01hspyadexample001",
            "ad_id": "120212345678901234",
            "brand_id": "brd_01hspybrandex001",
            "title": "Summer launch",
            "description": "Shop the drop",
            "live": true,
            "publisher_platform": ["facebook"],
            "niches": ["fashion"],
            "categories": [],
            "languages": ["en"],
            "platform": "facebook"
          }
        ],
        "total": 1,
        "limit": 1,
        "offset": 0
      }
    }
    ```
    
    Do not page further unless the human asked. Never loop discovery unbounded.
    
    #### 4. Optional: Facebook Ad Library
    
    Also **1** credit on 2xx. `502 upstream_error` is a valid live outcome (retry once, then stop).
    
    ```bash
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      "${OBELISK_API_BASE}/partner/v1/facebook-ads/ads/search?query=test&limit=1" | jq .
    ```
    
    #### Next step
    
    Generate an image and download it. Re-check `/partner/v1/account` first — you just spent 1 credit.
    
    ### Generate an image and download it
    
    Create one image job with `external_id`, poll until terminal, then fetch a signed download URL.
    
    #### What you need
    
    - `OBELISK_API_KEY` / `OBELISK_API_BASE` as in the first quickstart.
    - Scope `generation` (in `DefaultScopes`).
    - `credits.available` ≥ the model's `credits.default_cost` (`nano-banana` basic = **30**).
    - Worker running if you want the job to leave `queued` (local: `./scripts/start.sh --no-wait`).
    
    ```bash
    export OBELISK_API_KEY
    export OBELISK_API_BASE="${OBELISK_API_BASE:-https://api.obeliskapp.io}"
    ```
    
    #### 1. Re-check account
    
    ```bash
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      "${OBELISK_API_BASE}/partner/v1/account" | jq '{scopes:.data.key.scopes, available:.data.credits.available, active:.data.subscription.active}'
    ```
    
    Require `generation` in scopes and `available >=` the model cost. State the planned cost before creating the job.
    
    #### 2. List models and pick by cost
    
    Free. No `X-Credit-Cost`.
    
    ```bash
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      "${OBELISK_API_BASE}/partner/v1/models" | jq .
    ```
    
    Expected `nano-banana` row:
    
    ```json
    {
      "ok": true,
      "code": 200,
      "message": "Models retrieved",
      "data": {
        "items": [
          {
            "id": "nano-banana",
            "mode": "image",
            "name": "Nano Banana",
            "provider": "kie",
            "credits": { "default_cost": 30 },
            "capabilities": {},
            "params": {}
          }
        ],
        "total": 1
      }
    }
    ```
    
    Pick the cheapest image model (`jq` sort by `credits.default_cost`). Examples below use `nano-banana`.
    
    #### 3. Create the job
    
    Always send `external_id` (`{agent_run_id}:{purpose}`). This call **reserves** 30 credits. Expect HTTP **202**, `data.status: queued`, `duplicate: false`. (Jobs are 202; posts are 201.)
    
    ```bash
    export RUN_ID="agent-run-1"
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      -H "Content-Type: application/json" \
      --data "{\"type\":\"image\",\"model\":\"nano-banana\",\"prompt\":\"partner smoke\",\"external_id\":\"${RUN_ID}:hero\"}" \
      "${OBELISK_API_BASE}/partner/v1/jobs" | jq .
    ```
    
    ```json
    {
      "ok": true,
      "code": 202,
      "message": "Job created",
      "data": {
        "id": "job_01hxyzexample0001",
        "status": "queued",
        "type": "image",
        "model": "nano-banana",
        "credits_reserved": 30,
        "external_id": "agent-run-1:hero",
        "duplicate": false
      }
    }
    ```
    
    Capture `JOB_ID` from `.data.id`.
    
    Replay of the **same** body is HTTP **200**, `duplicate: true`, same id, and does **not** charge again. Same `external_id` + a different prompt is **409** `external_id_conflict` — mint a new run id; do not force the old one.
    
    #### 4. Poll (5s × 24)
    
    Free. On any 429, sleep `Retry-After` (default 1s) and retry once. Watch `X-RateLimit-Remaining`.
    
    ```bash
    for i in $(seq 1 24); do
      curl -sS \
        -D - \
        -H "Authorization: Bearer ${OBELISK_API_KEY}" \
        "${OBELISK_API_BASE}/partner/v1/jobs/${JOB_ID}" | tee /tmp/obelisk-job.json | jq '.data.status'
      STATUS="$(jq -r '.data.status' /tmp/obelisk-job.json)"
      case "$STATUS" in completed|failed|cancelled) break ;; esac
      sleep 5
    done
    ```
    
    Expected completed body:
    
    ```json
    {
      "ok": true,
      "code": 200,
      "message": "Job retrieved",
      "data": {
        "id": "job_01hxyzexample0001",
        "status": "completed",
        "type": "image",
        "model": "nano-banana",
        "created_at": "2026-01-15T12:00:00Z",
        "completed_at": "2026-01-15T12:00:30Z",
        "credits_used": 30,
        "outputs": [
          {
            "media_id": "media_01hxyzexample0001",
            "url": "https://storage.example.com/signed",
            "content_type": "image/png"
          }
        ]
      }
    }
    ```
    
    `failed` / `cancelled` → stop; do not invent a media id. If 24 ticks elapse without a terminal status, report the last status and stop polling.
    
    #### 5. Download
    
    ```bash
    export MEDIA_ID="$(jq -r '.data.outputs[0].media_id' /tmp/obelisk-job.json)"
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      "${OBELISK_API_BASE}/partner/v1/media/${MEDIA_ID}/download" | jq .
    ```
    
    HTTP **200**, free:
    
    ```json
    {
      "ok": true,
      "code": 200,
      "message": "Download URL created",
      "data": {
        "media_id": "media_01hxyzexample0001",
        "url": "https://storage.example.com/signed",
        "expires_at": "2026-01-16T12:00:00Z"
      }
    }
    ```
    
    Then fetch the bytes (no API key on the signed host):
    
    ```bash
    curl -sS -L -o ./obelisk-output.bin "$(jq -r '.data.url' <<< "$DOWNLOAD_JSON")"
    ```
    
    #### Next step
    
    Post a TikTok draft with this `media_id`, or curate it into a collection (`POST /partner/v1/collections`).
    
    ### Post a TikTok draft
    
    Create a **draft-safe** TikTok post (`MEDIA_UPLOAD` + `SELF_ONLY`). Never default to a public post.
    
    #### What you need
    
    - `OBELISK_API_KEY` / `OBELISK_API_BASE` as in the first quickstart.
    - Scope **`publishing`**. This is **not** in `DefaultScopes` (`discovery`, `generation`, `assets`, `account`). A default-minted key gets `403 forbidden_scope` on `/accounts` and `/posts` — surface that to the human; do not retry.
    - A connected TikTok account with `capabilities.posting == true`. If none exist, **stop after listing accounts**. That is the documented "up to dispatch" gap: auth, scopes, and the accounts list are verified; nothing is posted.
    - A `media_id` the tenant owns (from the generate quickstart, or the optional upload below).
    - **1** credit per targeted account (charged on HTTP **201** only).
    
    ```bash
    export OBELISK_API_KEY
    export OBELISK_API_BASE="${OBELISK_API_BASE:-https://api.obeliskapp.io}"
    ```
    
    #### 1. Re-check account and scopes
    
    ```bash
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      "${OBELISK_API_BASE}/partner/v1/account" | jq '{scopes:.data.key.scopes, available:.data.credits.available}'
    ```
    
    If `publishing` is missing, **stop**. Tell the human to mint a key with `publishing` at `/settings/api-keys`.
    
    #### 2. List accounts
    
    ```bash
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      "${OBELISK_API_BASE}/partner/v1/accounts" | jq .
    ```
    
    HTTP **200** (or **403** `forbidden_scope` — treat as the scope gap above):
    
    ```json
    {
      "ok": true,
      "code": 200,
      "message": "Accounts retrieved",
      "data": {
        "items": [
          {
            "id": "conn_01htiktokexample01",
            "platform": "tiktok",
            "brand_id": "brand_01hxyzexample001",
            "username": "@acme",
            "display_name": "acme",
            "status": "active",
            "capabilities": { "posting": true }
          }
        ],
        "total": 1
      }
    }
    ```
    
    Select `platform=="tiktok"` and `capabilities.posting==true`.
    
    ##### If no TikTok with `posting==true`
    
    **Stop.** Tell the human to connect TikTok in the dashboard. Do **not** `POST /posts`. Do **not** invent an account id (`400 unknown_accounts`). You have completed the documented path up to dispatch.
    
    #### 3. Optional: upload media
    
    Skip if Generate already produced a `media_id`.
    
    ```bash
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      -F "file=@./hero.png;type=image/png" \
      "${OBELISK_API_BASE}/partner/v1/media/upload" | jq .
    ```
    
    HTTP **201**, `data.id` starts with `media_`. Capture `MEDIA_ID`.
    
    #### 4. Create the draft
    
    Always send `external_id`. Always `media.type: "slideshow"`, `tiktok.post_mode: "MEDIA_UPLOAD"`, `tiktok.privacy_level: "SELF_ONLY"`.
    
    Do not use `DIRECT_POST` or a public privacy level unless the human explicitly asked, and even then confirm. Every example here stays draft-only.
    
    Expect HTTP **201**, `duplicate: false`. Account rows typically `pending` (or `draft`) while the worker dispatches. Cost: **1** credit per targeted account (`X-Credit-Cost: 1` on 201).
    
    ```bash
    export RUN_ID="agent-run-1"
    export ACCOUNT_ID="<tiktok-account-id>"
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      -H "Content-Type: application/json" \
      --data "{\"caption\":\"Hello TikTok\",\"media\":{\"type\":\"slideshow\",\"media_ids\":[\"${MEDIA_ID}\"]},\"accounts\":[{\"id\":\"${ACCOUNT_ID}\"}],\"external_id\":\"${RUN_ID}:tiktok-draft\",\"tiktok\":{\"post_mode\":\"MEDIA_UPLOAD\",\"privacy_level\":\"SELF_ONLY\"}}" \
      "${OBELISK_API_BASE}/partner/v1/posts" | jq .
    ```
    
    ```json
    {
      "ok": true,
      "code": 201,
      "message": "Post created",
      "data": {
        "id": "post_01hxyzexample0001",
        "status": "draft",
        "duplicate": false,
        "accounts": {
          "total": 1,
          "states": [
            {
              "account_id": "conn_01htiktokexample01",
              "status": "pending",
              "published_at": null,
              "published_url": null,
              "error_message": null,
              "last_attempted_at": null
            }
          ]
        }
      }
    }
    ```
    
    Capture `POST_ID` from `.data.id`. Same body + same `external_id` replays as **200** `duplicate: true` with no second charge.
    
    #### 5. Get the post
    
    ```bash
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      "${OBELISK_API_BASE}/partner/v1/posts/${POST_ID}" | jq .
    ```
    
    ```json
    {
      "ok": true,
      "code": 200,
      "message": "Post retrieved",
      "data": {
        "id": "post_01hxyzexample0001",
        "caption": "Hello TikTok",
        "status": "draft",
        "created_at": "2026-01-15T12:00:00Z",
        "media": {
          "type": "slideshow",
          "media_ids": ["media_01hxyzexample0001"]
        },
        "tiktok": {
          "post_mode": "MEDIA_UPLOAD",
          "privacy_level": "SELF_ONLY",
          "disable_comment": false,
          "auto_add_music": false
        },
        "external_id": "agent-run-1:tiktok-draft",
        "accounts": {
          "total": 1,
          "states": [
            {
              "account_id": "conn_01htiktokexample01",
              "status": "pending",
              "published_at": null,
              "published_url": null,
              "error_message": null,
              "last_attempted_at": null
            }
          ]
        }
      }
    }
    ```
    
    #### 6. Retry failures only
    
    `POST /partner/v1/posts/retry` is free. Send only post ids whose account rows `failed`. `post_ids` max **20**. Do not retry pending/draft/posted rows.
    
    ```bash
    curl -sS \
      -H "Authorization: Bearer ${OBELISK_API_KEY}" \
      -H "Content-Type: application/json" \
      --data "{\"post_ids\":[\"${POST_ID}\"]}" \
      "${OBELISK_API_BASE}/partner/v1/posts/retry" | jq .
    ```
    
    ```json
    {
      "ok": true,
      "code": 200,
      "message": "Posts queued for retry",
      "data": {
        "retried": 1,
        "ids": ["post_01hxyzexample0001"],
        "failed": []
      }
    }
    ```
servers:
  - url: https://api.obeliskapp.io
    description: Production
  - url: http://127.0.0.1:8080
    description: Local development default
  - url: '{baseUrl}'
    description: Override host
    variables:
      baseUrl:
        default: http://127.0.0.1:8080
tags:
  - name: Account
    description: Key identity and tenant snapshot. Free.
  - name: Ad Spy
    description: Obelisk's curated ad index — search saved ads and the brands behind them.
  - name: Facebook Ads
    description: Meta Ad Library passthrough — ads, creative media, and advertising companies.
  - name: Google Ads
    description: Google Ads Transparency Center passthrough, by domain or advertiser.
  - name: Social Scrapers
    description: Public post, video, and tweet listings for a handle on Instagram, TikTok, and X.
  - name: Media
    description: Upload, import, list, download, and delete media assets. Free.
  - name: Collections
    description: Group media and ads into named collections. Free.
  - name: Brands
    description: Read the brands configured in the tenant. Free.
  - name: Products
    description: Read the products configured in the tenant. Free.
  - name: Models
    description: The generation models available to this key, with their credit costs.
  - name: Jobs
    description: Asynchronous image and video generation. Submit, poll, cancel.
  - name: Slideshows
    description: Multi-slide generation and per-slide export.
  - name: Accounts
    description: Connected social accounts available for publishing. Requires the publishing scope.
  - name: Posts
    description: Create, list, retry, and delete posts. TikTok drafts are the supported path.
  - name: Automations
    description: Recurring publishing rules and their run history.

# Sidebar information architecture. Scalar builds its grouped sidebar from
# x-tagGroups; the finer `tags` above become the endpoint lists inside each
# group. Adding a tag here without declaring it above (or vice versa) is caught
# by the validation script in specs/obelisk-api-docs-orgo-parity.md P2.
x-tagGroups:
  - name: Account
    tags: [Account]
  - name: Discovery
    tags: [Ad Spy, Facebook Ads, Google Ads, Social Scrapers]
  - name: Assets
    tags: [Media, Collections, Brands, Products]
  - name: Generation
    tags: [Models, Jobs, Slideshows]
  - name: Publishing
    tags: [Accounts, Posts, Automations]

security:
  - bearerAuth: []
paths:
  /partner/v1/ping:
    get:
      operationId: getPing
      summary: "Ping"
      description: "Confirms the partner key. Free. Scope account."
      tags: [Account]
      x-required-scope: account
      x-credit-cost: 0
      security:
        - bearerAuth: []
      responses:
        '200':
          description: "Key is valid"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessPing'
              example:
                ok: true
                code: 200
                message: pong
                data:
                  tenant_id: ten_01hxyzexample0001
                  key_public_id: x1y2z3abcdef
                  scopes:
                    - discovery
                    - generation
                    - assets
                    - account
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'

  /partner/v1/account:
    get:
      operationId: getAccount
      summary: "Get account"
      description: "Tenant, subscription, credits, and key snapshot. Free. Scope account."
      tags: [Account]
      x-required-scope: account
      x-credit-cost: 0
      security:
        - bearerAuth: []
      responses:
        '200':
          description: "Account retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessAccount'
              example:
                ok: true
                code: 200
                message: Account retrieved
                data:
                  tenant:
                    id: ten_01hxyzexample0001
                    name: Acme Studio
                  subscription:
                    active: true
                    tier: starter
                    product_id: prod_starter
                  credits:
                    total_purchased: 1000
                    total_consumed: 42
                    available: 958
                  key:
                    public_id: x1y2z3abcdef
                    scopes:
                      - discovery
                      - generation
                      - assets
                      - account
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'

  /partner/v1/payments/{tx_id}/receipt:
    get:
      operationId: getPaymentReceipt
      summary: "Get payment receipt"
      description: "ES256 JWT for a settled, tenant-scoped payment_transactions row. Free. Scope account. Keyless rows are inline-only (this route returns 404). Cross-tenant is 404 not_found, never 403. Verify against https://obeliskapp.io/developers/receipt-public-key.pem."
      tags: [Account]
      x-required-scope: account
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TxId'
      responses:
        '200':
          description: "Receipt retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessPaymentReceipt'
              example:
                ok: true
                code: 200
                message: Receipt retrieved
                data:
                  receipt: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eF9pZCI6InBheXR4XzAxIn0.sig
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'

  /partner/v1/spy/ads:
    get:
      operationId: listSpyAds
      summary: "List spy ads"
      description: "Paginated spy ads. Costs 1 credit on 2xx."
      tags: [Ad Spy]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: live
          in: query
          schema:
            type: boolean
        - name: platform
          in: query
          schema:
            type: string
        - name: brand_id
          in: query
          schema:
            type: string
        - name: display_format
          in: query
          schema:
            type: string
        - name: niche
          in: query
          schema:
            type: string
        - name: market_target
          in: query
          schema:
            type: string
        - name: language
          in: query
          schema:
            type: string
      responses:
        '200':
          description: "Ads retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessSpyAdPage'
              example:
                ok: true
                code: 200
                message: Ads retrieved
                data:
                  items:
                    - id: ad_01hspyadexample001
                      ad_id: '120212345678901234'
                      brand_id: brd_01hspybrandex001
                      title: Summer launch
                      description: Shop the drop
                      live: true
                      publisher_platform:
                        - facebook
                      niches:
                        - fashion
                      categories: []
                      languages:
                        - en
                      platform: facebook
                  total: 1
                  limit: 1
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/spy/ads/{id}:
    get:
      operationId: getSpyAd
      summary: "Get spy ad"
      description: "Single spy ad. Costs 1 credit on 2xx."
      tags: [Ad Spy]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Ad retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessSpyAd'
              example:
                ok: true
                code: 200
                message: Ad retrieved
                data:
                  id: ad_01hspyadexample001
                  ad_id: '120212345678901234'
                  brand_id: brd_01hspybrandex001
                  title: Summer launch
                  live: true
                  publisher_platform:
                    - facebook
                  niches: []
                  categories: []
                  languages: []
                  platform: facebook
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/spy/brands/search:
    get:
      operationId: searchSpyBrands
      summary: "Search spy brands"
      description: "Search by query or domain. Costs 1 credit on 2xx."
      tags: [Ad Spy]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: query
          in: query
          schema:
            type: string
        - name: domain
          in: query
          schema:
            type: string
        - name: order
          in: query
          schema:
            type: string
      responses:
        '200':
          description: "Brands retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessSpyBrandPage'
              example:
                ok: true
                code: 200
                message: Brands retrieved
                data:
                  items:
                    - id: brd_01hspybrandex001
                      name: Acme
                      niches: []
                      websites: []
                      is_delegate_page: false
                      platform: facebook
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/spy/brands:
    get:
      operationId: listSpyBrands
      summary: "List spy brands"
      description: "Paginated spy brands. Costs 1 credit on 2xx."
      tags: [Ad Spy]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: include_ads
          in: query
          schema:
            type: boolean
        - name: require_url
          in: query
          schema:
            type: boolean
        - name: platform
          in: query
          schema:
            type: string
        - name: niches
          in: query
          schema:
            type: string
        - name: name
          in: query
          schema:
            type: string
        - name: category
          in: query
          schema:
            type: string
        - name: verification_status
          in: query
          schema:
            type: string
      responses:
        '200':
          description: "Brands retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessSpyBrandPage'
              example:
                ok: true
                code: 200
                message: Brands retrieved
                data:
                  items:
                    - id: brd_01hspybrandex001
                      name: Acme
                      niches: []
                      websites: []
                      is_delegate_page: false
                      platform: facebook
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/spy/brands/{id}:
    get:
      operationId: getSpyBrand
      summary: "Get spy brand"
      description: "Single spy brand. Costs 1 credit on 2xx."
      tags: [Ad Spy]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
        - name: include_ads
          in: query
          schema:
            type: boolean
      responses:
        '200':
          description: "Brand retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessSpyBrand'
              example:
                ok: true
                code: 200
                message: Brand retrieved
                data:
                  id: brd_01hspybrandex001
                  name: Acme
                  niches: []
                  websites: []
                  is_delegate_page: false
                  platform: facebook
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/facebook-ads/ads/search:
    get:
      operationId: searchFacebookAds
      summary: "Search Facebook ads"
      description: "Facebook Ad Library search. Costs 1 credit on 2xx."
      tags: [Facebook Ads]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: query
          in: query
          schema:
            type: string
        - name: country
          in: query
          schema:
            type: string
        - name: ad_active_status
          in: query
          schema:
            type: string
        - name: media_type
          in: query
          schema:
            type: string
        - name: start_date
          in: query
          schema:
            type: string
        - name: end_date
          in: query
          schema:
            type: string
        - name: publisher_platform
          in: query
          description: Comma-separated publisher platforms
          schema:
            type: string
      responses:
        '200':
          description: "Ads retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessFacebookAdPage'
              example:
                ok: true
                code: 200
                message: Ads retrieved
                data:
                  items:
                    - id: '120212345678901234'
                      page_name: Acme
                      page_id: '1234567890'
                      is_active: true
                      start_date: '2026-01-01T00:00:00Z'
                      platforms:
                        - FACEBOOK
                      media_assets: []
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/facebook-ads/ads/{archive_id}/media:
    get:
      operationId: getFacebookAdMedia
      summary: "Get Facebook ad media"
      description: "Media assets for an archive id. Costs 1 credit on 2xx."
      tags: [Facebook Ads]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ArchiveId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: "Media retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessMediaAssetPage'
              example:
                ok: true
                code: 200
                message: Media retrieved
                data:
                  items:
                    - type: video
                      url: https://cdn.example.com/ad.mp4
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/facebook-ads/ads/{archive_id}:
    get:
      operationId: getFacebookAd
      summary: "Get Facebook ad"
      description: "Facebook ad details. Costs 1 credit on 2xx."
      tags: [Facebook Ads]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ArchiveId'
      responses:
        '200':
          description: "Ad retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessFacebookAd'
              example:
                ok: true
                code: 200
                message: Ad retrieved
                data:
                  id: '120212345678901234'
                  page_name: Acme
                  page_id: '1234567890'
                  is_active: true
                  start_date: '2026-01-01T00:00:00Z'
                  platforms:
                    - FACEBOOK
                  media_assets: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/facebook-ads/companies/search:
    get:
      operationId: searchFacebookCompanies
      summary: "Search Facebook companies"
      description: "Company/page search. Costs 1 credit on 2xx."
      tags: [Facebook Ads]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: query
          in: query
          schema:
            type: string
        - name: country
          in: query
          schema:
            type: string
      responses:
        '200':
          description: "Companies retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessFacebookCompanyPage'
              example:
                ok: true
                code: 200
                message: Companies retrieved
                data:
                  items:
                    - page_id: '1234567890'
                      name: Acme
                      facebook_likes: 12000
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/facebook-ads/companies/{page_id}/ads:
    get:
      operationId: listFacebookCompanyAds
      summary: "List Facebook company ads"
      description: "Ads for a page. Costs 1 credit on 2xx."
      tags: [Facebook Ads]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PageId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: country
          in: query
          schema:
            type: string
        - name: ad_active_status
          in: query
          schema:
            type: string
        - name: media_type
          in: query
          schema:
            type: string
        - name: publisher_platform
          in: query
          description: Comma-separated publisher platforms
          schema:
            type: string
      responses:
        '200':
          description: "Ads retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessFacebookAdPage'
              example:
                ok: true
                code: 200
                message: Ads retrieved
                data:
                  items:
                    - id: '120212345678901234'
                      page_name: Acme
                      page_id: '1234567890'
                      is_active: true
                      start_date: '2026-01-01T00:00:00Z'
                      platforms:
                        - FACEBOOK
                      media_assets: []
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/facebook-ads/platforms:
    get:
      operationId: getFacebookPlatforms
      summary: "Facebook Ad Library catalog"
      description: "Static publisher/media/status/country catalog. Free."
      tags: [Facebook Ads]
      x-required-scope: discovery
      x-credit-cost: 0
      x-keyless-x402: true
      security:
        - bearerAuth: []
      responses:
        '200':
          description: "Platforms retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessFacebookPlatforms'
              example:
                ok: true
                code: 200
                message: Platforms retrieved
                data:
                  publisher_platforms:
                    - FACEBOOK
                    - INSTAGRAM
                    - MESSENGER
                    - WHATSAPP
                  media_types:
                    - IMAGE
                    - VIDEO
                    - ALL
                  ad_active_statuses:
                    - ACTIVE
                    - INACTIVE
                    - ALL
                  countries:
                    US: United States
                    CA: Canada
                    GB: United Kingdom
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/google-ads/companies/{domain}/ads:
    get:
      operationId: listGoogleCompanyAds
      summary: "List Google ads by domain"
      description: "Google Ads Transparency ads for a domain. Costs 1 credit on 2xx."
      tags: [Google Ads]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PathDomain'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: topic
          in: query
          schema:
            type: string
        - name: region
          in: query
          schema:
            type: string
        - name: start_date
          in: query
          schema:
            type: string
        - name: end_date
          in: query
          schema:
            type: string
        - name: get_ad_details
          in: query
          schema:
            type: boolean
      responses:
        '200':
          description: "Ads retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessGoogleAdPage'
              example:
                ok: true
                code: 200
                message: Ads retrieved
                data:
                  items:
                    - advertiser_id: AR123
                      creative_id: CR456
                      first_shown: '2026-01-01T00:00:00Z'
                      last_shown: '2026-02-01T00:00:00Z'
                      media_assets: []
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/google-ads/advertisers/{advertiser_id}/ads:
    get:
      operationId: listGoogleAdvertiserAds
      summary: "List Google ads by advertiser"
      description: "Google Ads Transparency ads for an advertiser. Costs 1 credit on 2xx."
      tags: [Google Ads]
      x-required-scope: discovery
      x-credit-cost: 1
      x-keyless-x402: true
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AdvertiserId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: trim
          in: query
          schema:
            type: boolean
      responses:
        '200':
          description: "Ads retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessGoogleAdPage'
              example:
                ok: true
                code: 200
                message: Ads retrieved
                data:
                  items:
                    - advertiser_id: AR123
                      creative_id: CR456
                      first_shown: '2026-01-01T00:00:00Z'
                      last_shown: '2026-02-01T00:00:00Z'
                      media_assets: []
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/scraper/instagram/users/{handle}/posts:
    get:
      operationId: listInstagramPosts
      summary: "List Instagram posts"
      description: "Scraper bucket. Leading @ is stripped. Costs 2 credits on 2xx."
      tags: [Social Scrapers]
      x-required-scope: discovery
      x-credit-cost: 2
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Handle'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: "Posts retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessInstagramPostPage'
              example:
                ok: true
                code: 200
                message: Posts retrieved
                data:
                  items:
                    - id: '17900000000000000'
                      media_type: 1
                      taken_at: '2026-01-15T12:00:00Z'
                      like_count: 10
                      comment_count: 1
                      media_assets: []
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/scraper/tiktok/users/{handle}/videos:
    get:
      operationId: listTikTokVideos
      summary: "List TikTok videos"
      description: "Scraper bucket. Costs 2 credits on 2xx."
      tags: [Social Scrapers]
      x-required-scope: discovery
      x-credit-cost: 2
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Handle'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: sort_by
          in: query
          schema:
            type: string
      responses:
        '200':
          description: "Videos retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessTikTokVideoPage'
              example:
                ok: true
                code: 200
                message: Videos retrieved
                data:
                  items:
                    - video_id: '7000000000000000000'
                      create_time: '2026-01-15T12:00:00Z'
                      like_count: 10
                      comment_count: 1
                      share_count: 0
                      play_count: 100
                      duration: 12
                      media_assets: []
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/scraper/twitter/users/{handle}/tweets:
    get:
      operationId: listTwitterTweets
      summary: "List Twitter tweets"
      description: "Scraper bucket. Costs 2 credits on 2xx."
      tags: [Social Scrapers]
      x-required-scope: discovery
      x-credit-cost: 2
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Handle'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: "Tweets retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessTwitterTweetPage'
              example:
                ok: true
                code: 200
                message: Tweets retrieved
                data:
                  items:
                    - tweet_id: '1800000000000000000'
                      text: Hello world
                      created_at: '2026-01-15T12:00:00Z'
                      author_followers: 100
                      is_verified: false
                      like_count: 1
                      retweet_count: 0
                      reply_count: 0
                      quote_count: 0
                      bookmark_count: 0
                      view_count: 20
                      media_assets: []
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/media/upload:
    post:
      operationId: uploadMedia
      summary: "Upload media"
      description: "Multipart field `file`. Same 100M body limit as /v1/media/upload. Oversize is HTTP 413 invalid_payload. Free."
      tags: [Media]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: Image, video, or audio file
                name:
                  type: string
                brand_id:
                  type: string
                product_id:
                  type: string
      responses:
        '201':
          description: "Media uploaded"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessMedia'
              example:
                ok: true
                code: 201
                message: Media uploaded
                data:
                  id: media_01hxyzexample0001
                  type: image
                  name: smoke.png
                  content_type: image/png
                  size_bytes: 67
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'

  /partner/v1/media/import:
    post:
      operationId: importMedia
      summary: "Import media from URL"
      description: "Import a remote image/video/audio URL. Free."
      tags: [Media]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportMediaRequest'
            example:
              url: https://cdn.example.com/ad.png
      responses:
        '201':
          description: "Media imported"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessMedia'
              example:
                ok: true
                code: 201
                message: Media imported
                data:
                  id: media_01hxyzexample0001
                  type: image
                  name: smoke.png
                  content_type: image/png
                  size_bytes: 67
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/media:
    get:
      operationId: listMedia
      summary: "List media"
      description: "Paginated tenant media. Free."
      tags: [Media]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: type
          in: query
          schema:
            type: string
            enum: [image, video, audio]
        - name: brand_id
          in: query
          schema:
            type: string
        - name: product_id
          in: query
          schema:
            type: string
      responses:
        '200':
          description: "Media retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessMediaPage'
              example:
                ok: true
                code: 200
                message: Media retrieved
                data:
                  items:
                    - id: media_01hxyzexample0001
                      type: image
                      name: smoke.png
                      content_type: image/png
                      size_bytes: 67
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/media/{id}:
    get:
      operationId: getMedia
      summary: "Get media"
      description: "Single media row. Free."
      tags: [Media]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Media retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessMedia'
              example:
                ok: true
                code: 200
                message: Media retrieved
                data:
                  id: media_01hxyzexample0001
                  type: image
                  name: smoke.png
                  content_type: image/png
                  size_bytes: 67
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

    delete:
      operationId: deleteMedia
      summary: "Delete media"
      description: "Deletes a media object. Free."
      tags: [Media]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Media deleted"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessDeletedMedia'
              example:
                ok: true
                code: 200
                message: Media deleted
                data:
                  id: media_01hxyzexample0001
                  deleted: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/media/{id}/download:
    get:
      operationId: downloadMedia
      summary: "Create media download URL"
      description: "24h signed URL. Free."
      tags: [Media]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Download URL created"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessMediaDownload'
              example:
                ok: true
                code: 200
                message: Download URL created
                data:
                  media_id: media_01hxyzexample0001
                  url: https://storage.example.com/signed
                  expires_at: '2026-01-16T12:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/collections:
    get:
      operationId: listCollections
      summary: "List collections"
      description: "Paginated collections. Free."
      tags: [Collections]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: "Collections retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessCollectionPage'
              example:
                ok: true
                code: 200
                message: Collections retrieved
                data:
                  items:
                    - id: col_01hxyzexample0001
                      name: smoke-coll
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

    post:
      operationId: createCollection
      summary: "Create collection"
      description: "Creates a collection. Free."
      tags: [Collections]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCollectionRequest'
            example:
              name: smoke-coll
      responses:
        '201':
          description: "Collection created"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessCollection'
              example:
                ok: true
                code: 201
                message: Collection created
                data:
                  id: col_01hxyzexample0001
                  name: smoke-coll
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/collections/{id}:
    get:
      operationId: getCollection
      summary: "Get collection"
      description: "Single collection. Free."
      tags: [Collections]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Collection retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessCollection'
              example:
                ok: true
                code: 200
                message: Collection retrieved
                data:
                  id: col_01hxyzexample0001
                  name: smoke-coll
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/collections/{id}/items:
    get:
      operationId: listCollectionItems
      summary: "List collection items"
      description: "Paginated membership rows. Free."
      tags: [Collections]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: "Collection items retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessCollectionItemPage'
              example:
                ok: true
                code: 200
                message: Collection items retrieved
                data:
                  items:
                    - item_type: media
                      resource_id: media_01hxyzexample0001
                      position: 0
                      added_at: '2026-01-15T12:00:00Z'
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

    post:
      operationId: addCollectionItems
      summary: "Add collection items"
      description: "Partial success: added[] and failed{}. Free."
      tags: [Collections]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddItemsRequest'
            example:
              items:
                - item_type: media
                  resource_id: media_01hxyzexample0001
      responses:
        '200':
          description: "Items added"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessAddItemsResult'
              example:
                ok: true
                code: 200
                message: Items added
                data:
                  added:
                    - item_type: media
                      resource_id: media_01hxyzexample0001
                      position: 0
                      added_at: '2026-01-15T12:00:00Z'
                  failed: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/collections/{id}/items/{item_type}/{resource_id}:
    delete:
      operationId: removeCollectionItem
      summary: "Remove collection item"
      description: "Removes one membership row. Free."
      tags: [Collections]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/ItemType'
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: "Item removed"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessRemovedItem'
              example:
                ok: true
                code: 200
                message: Item removed
                data:
                  removed: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/brands:
    get:
      operationId: listBrands
      summary: "List brands"
      description: "Tenant brands (no OAuth credentials). Free."
      tags: [Brands]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: "Brands retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessBrandPage'
              example:
                ok: true
                code: 200
                message: Brands retrieved
                data:
                  items:
                    - id: brand_01hxyzexample001
                      name: Acme
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/brands/{id}:
    get:
      operationId: getBrand
      summary: "Get brand"
      description: "Single brand. Free."
      tags: [Brands]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Brand retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessBrand'
              example:
                ok: true
                code: 200
                message: Brand retrieved
                data:
                  id: brand_01hxyzexample001
                  name: Acme
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/products:
    get:
      operationId: listProducts
      summary: "List products"
      description: "Tenant products. Free."
      tags: [Products]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: "Products retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessProductPage'
              example:
                ok: true
                code: 200
                message: Products retrieved
                data:
                  items:
                    - id: prod_01hxyzexample001
                      brand_id: brand_01hxyzexample001
                      name: Widget
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/products/{id}:
    get:
      operationId: getProduct
      summary: "Get product"
      description: "Single product. Free."
      tags: [Products]
      x-required-scope: assets
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Product retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessProduct'
              example:
                ok: true
                code: 200
                message: Product retrieved
                data:
                  id: prod_01hxyzexample001
                  brand_id: brand_01hxyzexample001
                  name: Widget
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/models:
    get:
      operationId: listModels
      summary: "List generation models"
      description: "Catalog rows. Filter with mode=image|video|audio. items+total only (no limit/offset). Free."
      tags: [Models]
      x-required-scope: generation
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - name: mode
          in: query
          schema:
            type: string
            enum: [image, video, audio]
      responses:
        '200':
          description: "Models retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessModelList'
              example:
                ok: true
                code: 200
                message: Models retrieved
                data:
                  items:
                    - id: nano-banana
                      mode: image
                      name: Nano Banana
                      provider: kie
                      credits:
                        default_cost: 30
                      capabilities: {}
                      params: {}
                  total: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/jobs:
    post:
      operationId: createJob
      summary: "Create generation job"
      description: "Variable credit cost reserved at create. Replay-200 when external_id matches; 409 external_id_conflict on fingerprint mismatch."
      tags: [Jobs]
      x-required-scope: generation
      x-credit-cost: "variable"
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateJobRequest'
            example:
              type: image
              model: nano-banana
              prompt: partner smoke
              external_id: agent-run-1:hero
      responses:
        '202':
          description: "Job created (use 200 JobReplay for idempotent replay)"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessJobCreated'
              example:
                ok: true
                code: 202
                message: Job created
                data:
                  id: job_01hxyzexample0001
                  status: queued
                  type: image
                  model: nano-banana
                  credits_reserved: 30
                  external_id: agent-run-1:hero
                  duplicate: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/ExternalIDConflict'
        '422':
          $ref: '#/components/responses/InvalidPayload'
        '200':
          $ref: '#/components/responses/JobReplay'

    get:
      operationId: listJobs
      summary: "List generation jobs"
      description: "Paginated jobs. status=queued|running|completed|failed|cancelled. type=image|video. Free."
      tags: [Jobs]
      x-required-scope: generation
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: status
          in: query
          schema:
            type: string
            enum: [queued, running, completed, failed, cancelled]
        - name: type
          in: query
          schema:
            type: string
            enum: [image, video]
      responses:
        '200':
          description: "Jobs retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessJobPage'
              example:
                ok: true
                code: 200
                message: Jobs retrieved
                data:
                  items:
                    - id: job_01hxyzexample0001
                      status: completed
                      type: image
                      model: nano-banana
                      created_at: '2026-01-15T12:00:00Z'
                      completed_at: '2026-01-15T12:00:30Z'
                      credits_used: 30
                      outputs:
                        - media_id: media_01hxyzexample0001
                          url: https://storage.example.com/signed
                          content_type: image/png
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/jobs/{id}:
    get:
      operationId: getJob
      summary: "Get generation job"
      description: "Poll until completed|failed|cancelled. Free."
      tags: [Jobs]
      x-required-scope: generation
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Job retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessJobStatus'
              example:
                ok: true
                code: 200
                message: Job retrieved
                data:
                  id: job_01hxyzexample0001
                  status: completed
                  type: image
                  model: nano-banana
                  created_at: '2026-01-15T12:00:00Z'
                  completed_at: '2026-01-15T12:00:30Z'
                  credits_used: 30
                  outputs:
                    - media_id: media_01hxyzexample0001
                      url: https://storage.example.com/signed
                      content_type: image/png
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      operationId: cancelJob
      summary: "Cancel generation job"
      description: "Cancels a non-terminal job and refunds reserved credits. Terminal jobs return 400 validation_failed. Free."
      tags: [Jobs]
      x-required-scope: generation
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Job cancelled"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessJobStatus'
              example:
                ok: true
                code: 200
                message: Job cancelled
                data:
                  id: job_01hxyzexample0001
                  status: cancelled
                  type: image
                  model: nano-banana
                  created_at: '2026-01-15T12:00:00Z'
                  credits_used: 0
                  outputs: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '400':
          $ref: '#/components/responses/ValidationFailed'

  /partner/v1/slideshows/generate:
    post:
      operationId: generateSlideshow
      summary: "Generate slideshow"
      description: "Starts a slideshow job. Costs 10 credits on 2xx."
      tags: [Slideshows]
      x-required-scope: generation
      x-credit-cost: 10
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateSlideshowRequest'
            example:
              prompt: Launch story
              slide_count: 6
      responses:
        '202':
          description: "Slideshow generation started"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessSlideshow'
              example:
                ok: true
                code: 202
                message: Slideshow generation started
                data:
                  id: job_01hslideshowex001
                  title: Launch story
                  status: queued
                  slide_count: 6
                  slides: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/slideshows:
    get:
      operationId: listSlideshows
      summary: "List slideshows"
      description: "Paginated slideshows. Free."
      tags: [Slideshows]
      x-required-scope: generation
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: "Slideshows retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessSlideshowPage'
              example:
                ok: true
                code: 200
                message: Slideshows retrieved
                data:
                  items:
                    - id: ss_01hxyzexample0001
                      title: Launch story
                      status: completed
                      slide_count: 6
                      slides: []
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/slideshows/{id}:
    get:
      operationId: getSlideshow
      summary: "Get slideshow"
      description: "Slideshow or in-flight job projection. Free."
      tags: [Slideshows]
      x-required-scope: generation
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Slideshow retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessSlideshow'
              example:
                ok: true
                code: 200
                message: Slideshow retrieved
                data:
                  id: ss_01hxyzexample0001
                  title: Launch story
                  status: completed
                  slide_count: 1
                  slides:
                    - position: 0
                      text: Hello
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'

  /partner/v1/slideshows/{id}/slides/{position}/export:
    post:
      operationId: exportSlide
      summary: "Export slideshow slide"
      description: "Exports one slide to media. Costs 1 credit on 2xx."
      tags: [Slideshows]
      x-required-scope: generation
      x-credit-cost: 1
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/Position'
      responses:
        '200':
          description: "Slide exported"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessExportedAsset'
              example:
                ok: true
                code: 200
                message: Slide exported
                data:
                  media_id: media_01hxyzexample0001
                  url: https://storage.example.com/signed
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/accounts:
    get:
      operationId: listAccounts
      summary: "List publishing accounts"
      description: "Connected accounts. posting=true only for TikTok in v1. Requires publishing scope (not in DefaultScopes). items+total only. Free."
      tags: [Accounts]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      responses:
        '200':
          description: "Accounts retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessAccountList'
              example:
                ok: true
                code: 200
                message: Accounts retrieved
                data:
                  items:
                    - id: conn_01htiktokexample01
                      platform: tiktok
                      brand_id: brand_01hxyzexample001
                      username: '@acme'
                      display_name: acme
                      status: active
                      capabilities:
                        posting: true
                  total: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'

  /partner/v1/posts:
    post:
      operationId: createPost
      summary: "Create post"
      description: "TikTok-first. Costs 1 credit per targeted account on 201. Replay-200 skips the charge. external_id is immutable."
      tags: [Posts]
      x-required-scope: publishing
      x-credit-cost: "1-per-account"
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePostRequest'
            example:
              caption: Hello TikTok
              media:
                type: slideshow
                media_ids:
                  - media_01hxyzexample0001
              accounts:
                - id: conn_01htiktokexample01
              external_id: agent-run-1:tiktok-draft
              tiktok:
                post_mode: MEDIA_UPLOAD
                privacy_level: SELF_ONLY
      responses:
        '201':
          description: "Post created (use 200 PostReplay for idempotent replay)"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Credit-Cost:
              $ref: '#/components/headers/X-Credit-Cost'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessPostCreated'
              example:
                ok: true
                code: 201
                message: Post created
                data:
                  id: post_01hxyzexample0001
                  status: draft
                  duplicate: false
                  accounts:
                    total: 1
                    states:
                      - account_id: conn_01htiktokexample01
                        status: pending
                        published_at: null
                        published_url: null
                        error_message: null
                        last_attempted_at: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '400':
          $ref: '#/components/responses/PostBadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/ExternalIDConflict'
        '422':
          $ref: '#/components/responses/InvalidPayload'
        '200':
          $ref: '#/components/responses/PostReplay'

    get:
      operationId: listPosts
      summary: "List posts"
      description: "Paginated posts plus summary.by_status. since/until are RFC3339. Free."
      tags: [Posts]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: status
          in: query
          schema:
            type: string
        - name: since
          in: query
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: "Posts retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessPostList'
              example:
                ok: true
                code: 200
                message: Posts retrieved
                data:
                  summary:
                    total: 1
                    by_status:
                      draft: 1
                      pending: 0
                      scheduled: 0
                      posted: 0
                      failed: 0
                      partial: 0
                      canceled: 0
                  items:
                    - id: post_01hxyzexample0001
                      caption: Hello TikTok
                      status: draft
                      created_at: '2026-01-15T12:00:00Z'
                      media:
                        type: slideshow
                        count: 1
                      external_id: agent-run-1:tiktok-draft
                      accounts:
                        total: 1
                        posted: 0
                        failed: 0
                  total: 1
                  limit: 20
                  offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '400':
          $ref: '#/components/responses/PostBadRequest'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/posts/retry:
    post:
      operationId: retryPosts
      summary: "Retry posts"
      description: "Re-queues failed deliveries. Does not charge. post_ids max 20. Registered before /posts/{id}."
      tags: [Posts]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RetryPostsRequest'
            example:
              post_ids:
                - post_01hxyzexample0001
      responses:
        '200':
          description: "Posts queued for retry"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessRetryPosts'
              example:
                ok: true
                code: 200
                message: Posts queued for retry
                data:
                  retried: 1
                  ids:
                    - post_01hxyzexample0001
                  failed: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '400':
          $ref: '#/components/responses/PostBadRequest'
        '422':
          $ref: '#/components/responses/InvalidPayload'

  /partner/v1/posts/{id}:
    get:
      operationId: getPost
      summary: "Get post"
      description: "Full post including per-account states. Soft-deleted rows stay readable as canceled. Free."
      tags: [Posts]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Post retrieved"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessPostDetail'
              example:
                ok: true
                code: 200
                message: Post retrieved
                data:
                  id: post_01hxyzexample0001
                  caption: Hello TikTok
                  status: draft
                  created_at: '2026-01-15T12:00:00Z'
                  media:
                    type: slideshow
                    media_ids:
                      - media_01hxyzexample0001
                  tiktok:
                    post_mode: MEDIA_UPLOAD
                    privacy_level: SELF_ONLY
                    disable_comment: false
                    auto_add_music: false
                  external_id: agent-run-1:tiktok-draft
                  accounts:
                    total: 1
                    states:
                      - account_id: conn_01htiktokexample01
                        status: pending
                        published_at: null
                        published_url: null
                        error_message: null
                        last_attempted_at: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      operationId: deletePost
      summary: "Delete post"
      description: "Soft-delete (status=canceled). Blocked for posted|partial|canceled (400 validation_failed with issues.status). Free."
      tags: [Posts]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: "Post deleted"
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessPostDeleted'
              example:
                ok: true
                code: 200
                message: Post deleted
                data:
                  id: post_01hxyzexample0001
                  deleted: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
        '404':
          $ref: '#/components/responses/NotFound'
        '400':
          $ref: '#/components/responses/ValidationFailed'

  /partner/v1/automations:
    post:
      operationId: createAutomation
      summary: Create automation
      description: Recurring generate-to-publish pipeline. Free at HTTP; executor charges generation and publish later. 403 automation_limit when the active cap is exceeded.
      tags: [Automations]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, account_ids, schedule, hooks, generation_config, post_settings]
              properties:
                title:
                  type: string
                account_ids:
                  type: array
                  items:
                    type: string
                schedule:
                  type: array
                  items:
                    $ref: '#/components/schemas/AutomationScheduleEntry'
                hooks:
                  type: array
                  items:
                    type: string
                style_prompt:
                  type: string
                generation_config:
                  $ref: '#/components/schemas/AutomationGenerationConfig'
                post_settings:
                  $ref: '#/components/schemas/AutomationPostSettings'
      responses:
        '201':
          description: Automation created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '422':
          $ref: '#/components/responses/InvalidPayload'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
    get:
      operationId: listAutomations
      summary: List automations
      description: Offset-paginated automations. Free.
      tags: [Automations]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Automations retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'

  /partner/v1/automations/{id}/run:
    post:
      operationId: runAutomation
      summary: Run automation
      description: One-off manual trigger. Returns 202 {run_id}. Local execute is not auto-fired.
      tags: [Automations]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                hook:
                  type: string
                mode:
                  type: string
                  enum: [full, generate_only]
      responses:
        '202':
          description: Automation run accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'

  /partner/v1/automations/{id}/runs:
    get:
      operationId: listAutomationRuns
      summary: List automation runs
      tags: [Automations]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Automation runs retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'

  /partner/v1/automations/{id}/runs/{run_id}:
    get:
      operationId: getAutomationRun
      summary: Get automation run
      description: Exposes status, job_id, post_id, and error — the create-to-resource linkage ReelFarm lacks.
      tags: [Automations]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
        - name: run_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Automation run retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'

  /partner/v1/automations/{id}:
    get:
      operationId: getAutomation
      summary: Get automation
      description: Includes schedule job_ids, consecutive_failures, last_error, and the last 5 runs.
      tags: [Automations]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: Automation retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateAutomation
      summary: Update automation
      description: "Config update or {action: pause|unpause}. Mutually exclusive."
      tags: [Automations]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Automation updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/InvalidPayload'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteAutomation
      summary: Delete automation
      description: Soft-delete. Future sweep firings stop; run history remains in the database. Subsequent GET is 404.
      tags: [Automations]
      x-required-scope: publishing
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: Automation deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimitFamily'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        Partner API key in the form `osk_live_<public_id>.<secret>`
        (example `osk_live_x1y2z3abcdef.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`).
        Clerk JWTs are rejected with 401 unauthorized.

  headers:
    X-RateLimit-Limit:
      description: Max requests in the current 60s window (-1 = unlimited). Logical name; Go may canonicalize to X-Ratelimit-Limit.
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Requests left in the window (-1 = unlimited).
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Unix seconds when the oldest slot expires.
      schema:
        type: integer
        format: int64
    Retry-After:
      description: Seconds to wait after 429 rate_limited.
      schema:
        type: integer
    X-Credit-Cost:
      description: Credits charged for this 2xx response. Absent on free routes and replays.
      schema:
        type: integer
    X-Credits-Remaining:
      description: Credits remaining after this 2xx charge. Absent on free routes and replays.
      schema:
        type: integer

  parameters:
    Limit:
      name: limit
      in: query
      description: Page size. Default 20, clamped to 1–100.
      schema:
        type: integer
        default: 20
        minimum: 1
        maximum: 100
    Offset:
      name: offset
      in: query
      description: Page offset. Default 0.
      schema:
        type: integer
        default: 0
        minimum: 0
    Id:
      name: id
      in: path
      required: true
      schema:
        type: string
    TxId:
      name: tx_id
      in: path
      required: true
      description: Payment transaction id (`paytx_…`).
      schema:
        type: string
    ArchiveId:
      name: archive_id
      in: path
      required: true
      schema:
        type: string
    Handle:
      name: handle
      in: path
      required: true
      description: Username; leading @ is stripped.
      schema:
        type: string
    PathDomain:
      name: domain
      in: path
      required: true
      schema:
        type: string
    AdvertiserId:
      name: advertiser_id
      in: path
      required: true
      schema:
        type: string
    PageId:
      name: page_id
      in: path
      required: true
      schema:
        type: string
    Position:
      name: position
      in: path
      required: true
      schema:
        type: integer
    ItemType:
      name: item_type
      in: path
      required: true
      schema:
        type: string
    ResourceId:
      name: resource_id
      in: path
      required: true
      schema:
        type: string

  responses:
    Unauthorized:
      description: Missing or invalid partner API key (Clerk JWTs included).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          example:
            ok: false
            code: 401
            message: unauthorized
            error_code: unauthorized
            request_id: req_01hxyzexample
    PaymentRequired:
      description: >
        402 is subscription_required (no active plan; kill switch / no x402
        rail; emitted before rate-limit headers), insufficient_credits
        (issues.credits_required / issues.credits_remaining),
        payment_required (x402 rail; PAYMENT-REQUIRED header present), or
        spend_limit_exceeded (x402 key cap; no PAYMENT-REQUIRED header —
        agents stop; a human must raise the limit).
        Branch on error_code. payment_required is 403 when issues.reason is
        kyt_risk_detected or request_blocked_by_location.
      headers:
        PAYMENT-REQUIRED:
          description: Base64-encoded x402 v2 PaymentRequired (only on payment_required).
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          examples:
            subscription_required:
              value:
                ok: false
                code: 402
                message: "An active Obelisk subscription is required. See https://obeliskapp.io/pricing for current plans."
                error_code: subscription_required
                request_id: req_01hxyzexample
            insufficient_credits:
              value:
                ok: false
                code: 402
                message: "This request needs more credits than are currently available. See https://obeliskapp.io/pricing for current plans."
                error_code: insufficient_credits
                issues:
                  credits_required: 30
                  credits_remaining: 4
                request_id: req_01hxyzexample
            payment_required:
              value:
                ok: false
                code: 402
                message: payment required
                error_code: payment_required
                issues:
                  usd_cents: 1
                  credits_equiv: 1
                  docs: https://obeliskapp.io/developers/#x402
                request_id: req_01hxyzexample
            spend_limit_exceeded:
              summary: Bare 402 — no PAYMENT-REQUIRED header
              value:
                ok: false
                code: 402
                message: spend limit exceeded
                error_code: spend_limit_exceeded
                issues:
                  limit_usd_cents: 5000
                  window_days: 30
                  spent_usd_cents: 5000
                request_id: req_01hxyzexample
    ForbiddenScope:
      description: Key is missing the required scope.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          example:
            ok: false
            code: 403
            message: 'missing required scope: publishing'
            error_code: forbidden_scope
            request_id: req_01hxyzexample
    RateLimitFamily:
      description: >
        429 is either rate_limited (honor Retry-After) or concurrent_limit
        (issues.current / issues.max on generation create). Branch on error_code.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          examples:
            rate_limited:
              value:
                ok: false
                code: 429
                message: rate limited
                error_code: rate_limited
                request_id: req_01hxyzexample
            concurrent_limit:
              value:
                ok: false
                code: 429
                message: Too many concurrent jobs
                error_code: concurrent_limit
                issues:
                  current: 3
                  max: 3
                request_id: req_01hxyzexample
    InternalError:
      description: Unexpected failure. Message never leaks internals.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          example:
            ok: false
            code: 500
            message: internal error
            error_code: internal_error
            request_id: req_01hxyzexample
    NotFound:
      description: Missing resource or cross-tenant read (never 403).
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          example:
            ok: false
            code: 404
            message: not found
            error_code: not_found
            request_id: req_01hxyzexample
    InvalidPayload:
      description: >
        422 invalid_payload with formErrors/fieldErrors, or
        platform_not_supported when a reserved non-TikTok namespace is sent.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          example:
            ok: false
            code: 422
            message: Invalid payload
            error_code: invalid_payload
            issues:
              formErrors: []
              fieldErrors:
                file:
                  - file is required
            request_id: req_01hxyzexample
    ValidationFailed:
      description: 400 validation_failed (terminal job cancel, blocked post delete, etc.).
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          example:
            ok: false
            code: 400
            message: validation failed
            error_code: validation_failed
            issues:
              status: posted
            request_id: req_01hxyzexample
    ExternalIDConflict:
      description: Same external_id with a different payload fingerprint.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          example:
            ok: false
            code: 409
            message: external id conflict
            error_code: external_id_conflict
            request_id: req_01hxyzexample
    UpstreamError:
      description: Live provider failure. Message is always "upstream provider error".
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          example:
            ok: false
            code: 502
            message: upstream provider error
            error_code: upstream_error
            request_id: req_01hxyzexample
    PayloadTooLarge:
      description: Multipart body exceeded 100M.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          example:
            ok: false
            code: 413
            message: request body too large
            error_code: invalid_payload
            request_id: req_01hxyzexample
    PostBadRequest:
      description: >
        400 on publishing routes. Branch on error_code — unknown_accounts,
        media_unreachable, invalid_since, invalid_until, too_many_post_ids,
        or validation_failed (issues.status for blocked delete).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PartnerError'
          examples:
            unknown_accounts:
              value:
                ok: false
                code: 400
                message: unknown accounts
                error_code: unknown_accounts
                issues:
                  account_ids:
                    - conn_missing
                request_id: req_01hxyzexample
            media_unreachable:
              value:
                ok: false
                code: 400
                message: media unreachable
                error_code: media_unreachable
                issues:
                  urls:
                    - https://example.com/missing.png
                request_id: req_01hxyzexample
            invalid_since:
              value:
                ok: false
                code: 400
                message: invalid since
                error_code: invalid_since
                request_id: req_01hxyzexample
            invalid_until:
              value:
                ok: false
                code: 400
                message: invalid until
                error_code: invalid_until
                request_id: req_01hxyzexample
            too_many_post_ids:
              value:
                ok: false
                code: 400
                message: post_ids must contain at most 20 ids
                error_code: too_many_post_ids
                request_id: req_01hxyzexample
            validation_failed:
              value:
                ok: false
                code: 400
                message: validation failed
                error_code: validation_failed
                issues:
                  status: posted
                request_id: req_01hxyzexample
    JobReplay:
      description: Idempotent job replay (same external_id + fingerprint). No second charge.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SuccessJobCreated'
          example:
            ok: true
            code: 200
            message: Job already exists
            data:
              id: job_01hxyzexample0001
              status: queued
              type: image
              model: nano-banana
              credits_reserved: 30
              external_id: agent-run-1:hero
              duplicate: true
    PostReplay:
      description: Idempotent post replay (same external_id + fingerprint). No second charge.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SuccessPostCreated'
          example:
            ok: true
            code: 200
            message: Post already exists
            data:
              id: post_01hxyzexample0001
              status: draft
              duplicate: true
              accounts:
                total: 1
                states:
                  - account_id: conn_01htiktokexample01
                    status: pending
                    published_at: null
                    published_url: null
                    error_message: null
                    last_attempted_at: null

  schemas:
    PartnerSuccess:
      type: object
      required: [ok, code, message]
      properties:
        ok:
          type: boolean
          const: true
        code:
          type: integer
        message:
          type: string
        data: {}
    PartnerError:
      type: object
      required: [ok, code, message, error_code]
      properties:
        ok:
          type: boolean
          const: false
        code:
          type: integer
        message:
          type: string
        error_code:
          type: string
          enum:
          - unauthorized
          - subscription_required
          - insufficient_credits
          - payment_required
          - spend_limit_exceeded
          - forbidden_scope
          - not_found
          - bad_request
          - invalid_payload
          - validation_failed
          - external_id_conflict
          - unknown_accounts
          - platform_not_supported
          - media_unreachable
          - invalid_since
          - invalid_until
          - too_many_post_ids
          - rate_limited
          - concurrent_limit
          - automation_limit
          - internal_error
          - upstream_error
        issues:
          description: Shape depends on error_code.
          oneOf:
            - $ref: '#/components/schemas/ValidationIssues'
            - $ref: '#/components/schemas/CreditIssues'
            - $ref: '#/components/schemas/PaymentIssues'
            - $ref: '#/components/schemas/SpendLimitIssues'
            - $ref: '#/components/schemas/ConcurrentIssues'
            - $ref: '#/components/schemas/UnknownAccountsIssues'
            - $ref: '#/components/schemas/PlatformIssues'
            - $ref: '#/components/schemas/MediaUnreachableIssues'
            - $ref: '#/components/schemas/BlockedStatusIssues'
        request_id:
          type: string
    ValidationIssues:
      type: object
      required: [formErrors, fieldErrors]
      properties:
        formErrors:
          type: array
          items:
            type: string
        fieldErrors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
    CreditIssues:
      type: object
      properties:
        credits_required:
          type: integer
        credits_remaining:
          type: integer
    PaymentIssues:
      type: object
      required: [usd_cents, credits_equiv, docs]
      properties:
        usd_cents:
          type: integer
        credits_equiv:
          type: integer
        docs:
          type: string
          example: https://obeliskapp.io/developers/#x402
        reason:
          type: string
          description: Facilitator invalidReason pass-through (insufficient_funds, kyt_risk_detected, …).
    SpendLimitIssues:
      type: object
      required: [limit_usd_cents, window_days, spent_usd_cents]
      properties:
        limit_usd_cents:
          type: integer
        window_days:
          type: integer
        spent_usd_cents:
          type: integer
    ConcurrentIssues:
      type: object
      properties:
        current:
          type: integer
        max:
          type: integer
    UnknownAccountsIssues:
      type: object
      properties:
        account_ids:
          type: array
          items:
            type: string
    PlatformIssues:
      type: object
      properties:
        platform:
          type: string
    MediaUnreachableIssues:
      type: object
      properties:
        urls:
          type: array
          items:
            type: string
    BlockedStatusIssues:
      type: object
      properties:
        status:
          type: string
    PingData:
      type: object
      required: [tenant_id, key_public_id, scopes]
      properties:
        tenant_id:
          type: string
        key_public_id:
          type: string
        scopes:
          type: array
          items:
            type: string
    AccountData:
      type: object
      required: [tenant, subscription, credits, key]
      properties:
        tenant:
          $ref: '#/components/schemas/AccountTenant'
        subscription:
          $ref: '#/components/schemas/AccountSubscription'
        credits:
          $ref: '#/components/schemas/AccountCredits'
        key:
          $ref: '#/components/schemas/AccountKey'
        payment:
          $ref: '#/components/schemas/AccountPayment'
    PaymentReceiptData:
      type: object
      required: [receipt]
      properties:
        receipt:
          type: string
          description: Compact ES256 JWT. Verify against https://obeliskapp.io/developers/receipt-public-key.pem.
    PaymentResponse:
      type: object
      required: [success, transaction, network, payer]
      description: x402 v2 settlement-response (REST PAYMENT-RESPONSE header, MCP _meta x402/payment-response).
      properties:
        success:
          type: boolean
        transaction:
          type: string
        network:
          type: string
        payer:
          type: string
        extensions:
          type: object
          additionalProperties: true
          properties:
            receipt:
              type: string
              description: Compact ES256 JWT. Omitted when the receipt signer is unset.
    AccountPayment:
      type: object
      required: [rail, usd_cents_per_credit]
      description: Present only on the x402 rail (no active Polar subscription). Omitted for subscribers.
      properties:
        rail:
          type: string
          enum: [x402]
        usd_cents_per_credit:
          type: integer
        spend:
          $ref: '#/components/schemas/AccountSpend'
    AccountSpend:
      type: object
      required: [spent_usd_cents]
      description: Per-key x402 USD spend ceiling. Unlimited keys serialize limit/window/remaining as JSON null.
      properties:
        limit_usd_cents:
          type: integer
          nullable: true
        window_days:
          type: integer
          nullable: true
        spent_usd_cents:
          type: integer
        remaining_usd_cents:
          type: integer
          nullable: true
    AccountTenant:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
        name:
          type: string
    AccountSubscription:
      type: object
      required: [active, tier, product_id]
      properties:
        active:
          type: boolean
        tier:
          type: string
          enum: [starter, pro, enterprise]
        product_id:
          type: string
    AccountCredits:
      type: object
      required: [total_purchased, total_consumed, available]
      properties:
        total_purchased:
          type: integer
        total_consumed:
          type: integer
        available:
          type: integer
    AccountKey:
      type: object
      required: [public_id, scopes]
      properties:
        public_id:
          type: string
        scopes:
          type: array
          items:
            type: string
    MediaAsset:
      type: object
      required: [type, url]
      properties:
        type:
          type: string
        url:
          type: string
        preview_url:
          type: string
        width:
          type: integer
        height:
          type: integer
    SpyAd:
      type: object
      required: [id, brand_id, live, publisher_platform, niches, categories, languages, platform]
      properties:
        id:
          type: string
        ad_id:
          type: string
        brand_id:
          type: string
        title:
          type: string
        description:
          type: string
        live:
          type: boolean
        link_url:
          type: string
        cta_type:
          type: string
        cta_title:
          type: string
        type:
          type: string
        display_format:
          type: string
        publisher_platform:
          type: array
          items:
            type: string
        niches:
          type: array
          items:
            type: string
        product_category:
          type: string
        full_transcription:
          type:
            - string
            - "null"
        avatar:
          type: string
        categories:
          type: array
          items:
            type: string
        creative_targeting:
          type: string
        market_target:
          type: string
        languages:
          type: array
          items:
            type: string
        thumbnail:
          type: string
        thumbnail_url:
          type: string
        video_url:
          type: string
        image_url:
          type: string
        started_running:
          type: number
        platform:
          type: string
    SpyBrand:
      type: object
      required: [id, name, niches, websites, is_delegate_page, platform]
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        category:
          type: string
        niches:
          type: array
          items:
            type: string
        verification_status:
          type: string
        url:
          type: string
        websites:
          type: array
          items:
            type: string
        avatar:
          type: string
        ad_library_id:
          type: string
        is_delegate_page:
          type: boolean
        platform:
          type: string
        ads:
          type: array
          items:
            $ref: '#/components/schemas/SpyAd'
    FacebookAd:
      type: object
      required: [id, page_name, page_id, is_active, start_date, platforms, media_assets]
      properties:
        id:
          type: string
        page_name:
          type: string
        page_id:
          type: string
        is_active:
          type: boolean
        start_date:
          type: string
          format: date-time
        end_date:
          type: string
          format: date-time
        ad_text:
          type: string
        display_format:
          type: string
        cta_text:
          type: string
        platforms:
          type: array
          items:
            type: string
        link_url:
          type: string
        spend:
          type: string
        media_assets:
          type: array
          items:
            $ref: '#/components/schemas/MediaAsset'
        creation_time:
          type: string
          format: date-time
        total_active_time:
          type: integer
          format: int64
        audience:
          $ref: '#/components/schemas/Audience'
        branded_content:
          $ref: '#/components/schemas/BrandedContent'
    Audience:
      type: object
      properties:
        gender_audience:
          type: string
        age_range:
          $ref: '#/components/schemas/AgeRange'
        location_targets:
          type: array
          items:
            $ref: '#/components/schemas/LocationTarget'
        total_reach:
          type: integer
    AgeRange:
      type: object
      required: [min, max]
      properties:
        min:
          type: integer
        max:
          type: integer
    LocationTarget:
      type: object
      required: [name, excluded]
      properties:
        name:
          type: string
        type:
          type: string
        excluded:
          type: boolean
    BrandedContent:
      type: object
      properties:
        sponsor_name:
          type: string
        sponsor_page_id:
          type: string
        sponsor_profile_url:
          type: string
    FacebookInstagram:
      type: object
      required: [username, followers, is_verified]
      properties:
        username:
          type: string
        followers:
          type: integer
          format: int64
        is_verified:
          type: boolean
    FacebookCompany:
      type: object
      required: [page_id, name, facebook_likes]
      properties:
        page_id:
          type: string
        name:
          type: string
        category:
          type: string
        profile_image_url:
          type: string
        verification:
          type: string
        facebook_likes:
          type: integer
          format: int64
        instagram:
          $ref: '#/components/schemas/FacebookInstagram'
    FacebookPlatforms:
      type: object
      required: [publisher_platforms, media_types, ad_active_statuses, countries]
      properties:
        publisher_platforms:
          type: array
          items:
            type: string
        media_types:
          type: array
          items:
            type: string
        ad_active_statuses:
          type: array
          items:
            type: string
        countries:
          type: object
          additionalProperties:
            type: string
    GoogleAd:
      type: object
      required: [advertiser_id, creative_id, first_shown, last_shown, media_assets]
      properties:
        advertiser_id:
          type: string
        creative_id:
          type: string
        format:
          type: string
        ad_url:
          type: string
        advertiser_name:
          type: string
        domain:
          type: string
        image_url:
          type: string
        first_shown:
          type: string
          format: date-time
        last_shown:
          type: string
          format: date-time
        media_assets:
          type: array
          items:
            $ref: '#/components/schemas/MediaAsset'
    InstagramPost:
      type: object
      required: [id, media_type, taken_at, like_count, comment_count, media_assets]
      properties:
        id:
          type: string
        code:
          type: string
        media_type:
          type: integer
        taken_at:
          type: string
          format: date-time
        caption:
          type: string
        like_count:
          type: integer
        comment_count:
          type: integer
        play_count:
          type: integer
        url:
          type: string
        username:
          type: string
        media_assets:
          type: array
          items:
            $ref: '#/components/schemas/MediaAsset'
    TikTokVideo:
      type: object
      required: [video_id, create_time, like_count, comment_count, share_count, play_count, duration, media_assets]
      properties:
        video_id:
          type: string
        description:
          type: string
        create_time:
          type: string
          format: date-time
        author_username:
          type: string
        author_nickname:
          type: string
        like_count:
          type: integer
          format: int64
        comment_count:
          type: integer
          format: int64
        share_count:
          type: integer
          format: int64
        play_count:
          type: integer
          format: int64
        duration:
          type: integer
        thumbnail_url:
          type: string
        video_url:
          type: string
        media_assets:
          type: array
          items:
            $ref: '#/components/schemas/MediaAsset'
    TwitterTweet:
      type: object
      required: [tweet_id, text, created_at, author_followers, is_verified, like_count, retweet_count, reply_count, quote_count, bookmark_count, view_count, media_assets]
      properties:
        tweet_id:
          type: string
        text:
          type: string
        created_at:
          type: string
          format: date-time
        author_name:
          type: string
        author_handle:
          type: string
        author_followers:
          type: integer
        is_verified:
          type: boolean
        like_count:
          type: integer
        retweet_count:
          type: integer
        reply_count:
          type: integer
        quote_count:
          type: integer
        bookmark_count:
          type: integer
        view_count:
          type: integer
        url:
          type: string
        media_assets:
          type: array
          items:
            $ref: '#/components/schemas/MediaAsset'
    Media:
      type: object
      required: [id, type, name, content_type, size_bytes]
      properties:
        id:
          type: string
        type:
          type: string
        name:
          type: string
        content_type:
          type: string
        size_bytes:
          type: integer
          format: int64
        width:
          type: integer
        height:
          type: integer
        duration_seconds:
          type: number
        brand_id:
          type: string
        product_id:
          type: string
        thumbnail_url:
          type: string
    MediaDownload:
      type: object
      required: [media_id, url, expires_at]
      properties:
        media_id:
          type: string
        url:
          type: string
        expires_at:
          type: string
          format: date-time
    DeletedMedia:
      type: object
      required: [id, deleted]
      properties:
        id:
          type: string
        deleted:
          type: boolean
    ImportMediaRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
        brand_id:
          type: string
        product_id:
          type: string
        name:
          type: string
    Collection:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
    ItemPreview:
      type: object
      properties:
        name:
          type: string
        thumbnail_url:
          type: string
    CollectionItem:
      type: object
      required: [item_type, resource_id, position, added_at]
      properties:
        item_type:
          type: string
        resource_id:
          type: string
        position:
          type: integer
        added_at:
          type: string
          format: date-time
        preview:
          $ref: '#/components/schemas/ItemPreview'
    CreateCollectionRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
        description:
          type: string
    AddItemRequest:
      type: object
      required: [item_type, resource_id]
      properties:
        item_type:
          type: string
        resource_id:
          type: string
    AddItemsRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/AddItemRequest'
    AddItemsResult:
      type: object
      required: [added, failed]
      properties:
        added:
          type: array
          items:
            $ref: '#/components/schemas/CollectionItem'
        failed:
          type: object
          additionalProperties:
            type: string
    RemovedItem:
      type: object
      required: [removed]
      properties:
        removed:
          type: boolean
    Brand:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        colors:
          type: object
          additionalProperties:
            type: string
        fonts:
          type: object
          additionalProperties:
            type: string
        links:
          type: object
          additionalProperties:
            type: string
    Product:
      type: object
      required: [id, brand_id, name]
      properties:
        id:
          type: string
        brand_id:
          type: string
        name:
          type: string
        description:
          type: string
        category:
          type: string
    ModelCredits:
      type: object
      required: [default_cost]
      properties:
        default_cost:
          type: integer
        notes:
          type: string
    Model:
      type: object
      required: [id, mode, name, provider, credits, capabilities, params]
      properties:
        id:
          type: string
        mode:
          type: string
        name:
          type: string
        provider:
          type: string
        credits:
          $ref: '#/components/schemas/ModelCredits'
        capabilities:
          type: object
          additionalProperties: true
        params:
          type: object
          additionalProperties: true
    ModelList:
      type: object
      required: [items, total]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Model'
        total:
          type: integer
    CreateJobRequest:
      type: object
      required: [type, model, prompt]
      properties:
        type:
          type: string
          enum: [image, video]
        model:
          type: string
        prompt:
          type: string
        params:
          type: object
          additionalProperties: true
        brand_id:
          type: string
        product_id:
          type: string
        external_id:
          type: string
    JobCreated:
      type: object
      required: [id, status, type, model, credits_reserved, duplicate]
      properties:
        id:
          type: string
        status:
          type: string
        type:
          type: string
        model:
          type: string
        credits_reserved:
          type: integer
        external_id:
          type: string
        duplicate:
          type: boolean
    JobOutput:
      type: object
      required: [media_id, url, content_type]
      properties:
        media_id:
          type: string
        url:
          type: string
        content_type:
          type: string
    JobStatus:
      type: object
      required: [id, status, type, model, created_at, credits_used, outputs]
      properties:
        id:
          type: string
        status:
          type: string
        type:
          type: string
        model:
          type: string
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
        credits_used:
          type: integer
        external_id:
          type: string
        outputs:
          type: array
          items:
            $ref: '#/components/schemas/JobOutput'
        error:
          type: string
    GenerateSlideshowRequest:
      type: object
      required: [prompt]
      properties:
        prompt:
          type: string
        brand_id:
          type: string
        product_id:
          type: string
        slide_count:
          type: integer
        aspect_ratio:
          type: string
    Slide:
      type: object
      required: [position, text]
      properties:
        position:
          type: integer
        text:
          type: string
        font_size:
          type: integer
        font_color:
          type: string
        text_position:
          type: string
        offset_x:
          type: integer
        offset_y:
          type: integer
        rotation:
          type: number
    Slideshow:
      type: object
      required: [id, title, status, slide_count, slides]
      properties:
        id:
          type: string
        title:
          type: string
        status:
          type: string
        slide_count:
          type: integer
        slides:
          type: array
          items:
            $ref: '#/components/schemas/Slide'
    ExportedAsset:
      type: object
      required: [media_id, url]
      properties:
        media_id:
          type: string
        url:
          type: string
    AccountCapabilities:
      type: object
      required: [posting]
      properties:
        posting:
          type: boolean
    PublishingAccount:
      type: object
      required: [id, platform, brand_id, username, display_name, status, capabilities]
      properties:
        id:
          type: string
        platform:
          type: string
        brand_id:
          type: string
        username:
          type: string
        display_name:
          type: string
        status:
          type: string
        capabilities:
          $ref: '#/components/schemas/AccountCapabilities'
    PublishingAccountList:
      type: object
      required: [items, total]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/PublishingAccount'
        total:
          type: integer
    PostMedia:
      type: object
      required: [type]
      properties:
        type:
          type: string
        media_id:
          type: string
        media_ids:
          type: array
          items:
            type: string
        url:
          type: string
        urls:
          type: array
          items:
            type: string
    PostAccount:
      type: object
      required: [id]
      properties:
        id:
          type: string
    TikTokSettings:
      type: object
      properties:
        post_mode:
          type: string
          enum: [MEDIA_UPLOAD, DIRECT_POST]
        privacy_level:
          type: string
        disable_comment:
          type: boolean
        auto_add_music:
          type: boolean
        title:
          type: string
        description:
          type: string
    CreatePostRequest:
      type: object
      required: [caption, media, accounts]
      properties:
        caption:
          type: string
        media:
          $ref: '#/components/schemas/PostMedia'
        accounts:
          type: array
          items:
            $ref: '#/components/schemas/PostAccount'
        scheduled_at:
          type: string
        external_id:
          type: string
        tiktok:
          $ref: '#/components/schemas/TikTokSettings'
        youtube:
          description: Reserved. Sending a non-null value returns platform_not_supported.
        x:
          description: Reserved.
        instagram:
          description: Reserved.
        facebook:
          description: Reserved.
        pinterest:
          description: Reserved.
        meta:
          description: Reserved.
        music_url:
          type: string
    AccountState:
      type: object
      required: [account_id, status]
      properties:
        account_id:
          type: string
        status:
          type: string
        published_at:
          type:
            - string
            - "null"
        published_url:
          type:
            - string
            - "null"
        error_message:
          type:
            - string
            - "null"
        last_attempted_at:
          type:
            - string
            - "null"
    AccountStates:
      type: object
      required: [total, states]
      properties:
        total:
          type: integer
        states:
          type: array
          items:
            $ref: '#/components/schemas/AccountState'
    PostCreated:
      type: object
      required: [id, status, duplicate, accounts]
      properties:
        id:
          type: string
        status:
          type: string
        scheduled_at:
          type: string
        duplicate:
          type: boolean
        accounts:
          $ref: '#/components/schemas/AccountStates'
    PostSummaryMedia:
      type: object
      required: [type, count]
      properties:
        type:
          type: string
        count:
          type: integer
    PostSummaryAccounts:
      type: object
      required: [total, posted, failed]
      properties:
        total:
          type: integer
        posted:
          type: integer
        failed:
          type: integer
    PostSummary:
      type: object
      required: [id, caption, status, created_at, media, accounts]
      properties:
        id:
          type: string
        caption:
          type: string
        status:
          type: string
        scheduled_at:
          type: string
        created_at:
          type: string
          format: date-time
        media:
          $ref: '#/components/schemas/PostSummaryMedia'
        external_id:
          type: string
        accounts:
          $ref: '#/components/schemas/PostSummaryAccounts'
    PostListSummary:
      type: object
      required: [total, by_status]
      properties:
        total:
          type: integer
        by_status:
          type: object
          required: [draft, pending, scheduled, posted, failed, partial, canceled]
          properties:
            draft:
              type: integer
            pending:
              type: integer
            scheduled:
              type: integer
            posted:
              type: integer
            failed:
              type: integer
            partial:
              type: integer
            canceled:
              type: integer
    PostList:
      type: object
      required: [summary, items, total, limit, offset]
      properties:
        summary:
          $ref: '#/components/schemas/PostListSummary'
        items:
          type: array
          items:
            $ref: '#/components/schemas/PostSummary'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    PostDetail:
      type: object
      required: [id, caption, status, created_at, media, accounts]
      properties:
        id:
          type: string
        caption:
          type: string
        status:
          type: string
        scheduled_at:
          type: string
        created_at:
          type: string
          format: date-time
        media:
          $ref: '#/components/schemas/PostMedia'
        tiktok:
          $ref: '#/components/schemas/TikTokSettings'
        external_id:
          type: string
        accounts:
          $ref: '#/components/schemas/AccountStates'
    PostDeleted:
      type: object
      required: [id, deleted]
      properties:
        id:
          type: string
        deleted:
          type: boolean
    RetryPostsRequest:
      type: object
      properties:
        post_ids:
          type: array
          items:
            type: string
        account_ids:
          type: array
          items:
            type: string
    RetryPostsResult:
      type: object
      required: [retried, ids, failed]
      properties:
        retried:
          type: integer
        ids:
          type: array
          items:
            type: string
        failed:
          type: array
          items:
            type: string
        errors:
          type: object
          additionalProperties:
            type: string
    SpyAdPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/SpyAd'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    SpyBrandPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/SpyBrand'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    FacebookAdPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/FacebookAd'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    FacebookCompanyPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/FacebookCompany'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    MediaAssetPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/MediaAsset'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    GoogleAdPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/GoogleAd'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    InstagramPostPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/InstagramPost'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    TikTokVideoPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/TikTokVideo'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    TwitterTweetPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/TwitterTweet'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    MediaPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Media'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    CollectionPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Collection'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    CollectionItemPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CollectionItem'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    BrandPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Brand'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    ProductPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Product'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    JobPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/JobStatus'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    SlideshowPage:
      type: object
      required: [items, total, limit, offset]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Slideshow'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    SuccessPing:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/PingData'
    SuccessAccount:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/AccountData'
    SuccessPaymentReceipt:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/PaymentReceiptData'
    SuccessSpyAdPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/SpyAdPage'
    SuccessSpyAd:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/SpyAd'
    SuccessSpyBrandPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/SpyBrandPage'
    SuccessSpyBrand:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/SpyBrand'
    SuccessFacebookAdPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/FacebookAdPage'
    SuccessFacebookAd:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/FacebookAd'
    SuccessMediaAssetPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/MediaAssetPage'
    SuccessFacebookCompanyPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/FacebookCompanyPage'
    SuccessFacebookPlatforms:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/FacebookPlatforms'
    SuccessGoogleAdPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/GoogleAdPage'
    SuccessInstagramPostPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/InstagramPostPage'
    SuccessTikTokVideoPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/TikTokVideoPage'
    SuccessTwitterTweetPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/TwitterTweetPage'
    SuccessMedia:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/Media'
    SuccessMediaPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/MediaPage'
    SuccessMediaDownload:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/MediaDownload'
    SuccessDeletedMedia:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/DeletedMedia'
    SuccessCollection:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/Collection'
    SuccessCollectionPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/CollectionPage'
    SuccessCollectionItemPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/CollectionItemPage'
    SuccessAddItemsResult:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/AddItemsResult'
    SuccessRemovedItem:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/RemovedItem'
    SuccessBrand:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/Brand'
    SuccessBrandPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/BrandPage'
    SuccessProduct:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/Product'
    SuccessProductPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/ProductPage'
    SuccessModelList:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/ModelList'
    SuccessJobCreated:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/JobCreated'
    SuccessJobStatus:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/JobStatus'
    SuccessJobPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/JobPage'
    SuccessSlideshow:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/Slideshow'
    SuccessSlideshowPage:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/SlideshowPage'
    SuccessExportedAsset:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/ExportedAsset'
    SuccessAccountList:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/PublishingAccountList'
    SuccessPostCreated:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/PostCreated'
    SuccessPostList:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/PostList'
    SuccessPostDetail:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/PostDetail'
    SuccessPostDeleted:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/PostDeleted'
    SuccessRetryPosts:
      allOf:
        - $ref: '#/components/schemas/PartnerSuccess'
        - type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/RetryPostsResult'
    SuccessEnvelope:
      $ref: '#/components/schemas/PartnerSuccess'
    AutomationScheduleEntry:
      type: object
      required: [cron, tz]
      properties:
        job_id:
          type: string
        cron:
          type: string
        tz:
          type: string
    AutomationGenerationConfig:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [image, slideshow]
        model:
          type: string
        params:
          type: object
          additionalProperties: true
    AutomationPostSettings:
      type: object
      required: [mode]
      properties:
        mode:
          type: string
          enum: [draft, scheduled]
        caption:
          type: string
        tiktok:
          $ref: '#/components/schemas/TikTokSettings'
