{
  "info": {
    "_postman_id": "pixbix-public-v1",
    "name": "pixbix API v1",
    "description": "Public REST API for pixbix — template-driven image and video rendering, plus URL screenshots.\n\nSet `apiKey` in the environment before running anything. Import the matching environment from https://pixbix.app/postman/environment.json.\n\n**Rendering a template.** Two conditions, and both are checked on every render:\n\n1. Your workspace must OWN it. A template in the shared public library belongs to somebody else — copy it in with `POST /templates/{{templateId}}/duplicate` and render your copy. Rendering another workspace's template returns `403 TEMPLATE_NOT_OWNED`.\n2. It must be `published`. A draft returns `409 TEMPLATE_NOT_PUBLISHED`.\n\n`GET /v1/templates` reports `canRender` on every row, and `GET /v1/templates/:id` adds `renderBlockedReason` — check those rather than deriving the answer from `status` yourself.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "x-api-key",
        "type": "string"
      },
      {
        "key": "value",
        "value": "{{apiKey}}",
        "type": "string"
      },
      {
        "key": "in",
        "value": "header",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://pixbix.app/api",
      "type": "string"
    },
    {
      "key": "renderId",
      "value": "rnd_8f2a1c",
      "type": "string"
    },
    {
      "key": "templateId",
      "value": "diwali-sale-story",
      "type": "string"
    },
    {
      "key": "assetId",
      "value": "ast_19fa",
      "type": "string"
    },
    {
      "key": "presetId",
      "value": "prs_9d2f",
      "type": "string"
    },
    {
      "key": "id",
      "value": "diwali-sale-story",
      "type": "string"
    },
    {
      "key": "webhookId",
      "value": "whe_7c1f9a",
      "type": "string"
    },
    {
      "key": "workflowId",
      "value": "wfl_7c1f9a",
      "type": "string"
    },
    {
      "key": "runId",
      "value": "wfr_2b8e41",
      "type": "string"
    },
    {
      "key": "hookToken",
      "value": "whk_replace_me",
      "type": "string"
    },
    {
      "key": "spreadsheetId",
      "value": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
      "type": "string"
    },
    {
      "key": "fileId",
      "value": "1a2B3c4D5e6F",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "Image API",
      "description": "Render a template to PNG, JPEG, WebP or PDF. Stills come back synchronously.",
      "item": [
        {
          "name": "Render an image",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"templateId\": \"diwali-sale-story\",\n  \"values\": {\n    \"headline\": \"Diwali Sale\",\n    \"offer\": \"50% OFF\",\n    \"product\": \"https://cdn.acme.com/lamp.jpg\"\n  },\n  \"output\": {\n    \"format\": \"png\",\n    \"scale\": 2\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/render",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "render"
              ]
            },
            "description": "Renders a template to a still. Image renders resolve synchronously — the response carries the finished CDN URL rather than a job to poll. A template with a live-capture layer also returns `extracted`: whatever that layer read off the page it photographed, keyed as the template named it. A key whose selector matched nothing comes back `null`.\n\nRequired scope: `renders:write`\nCost: 1 image credit per render, per output scale.\n\nBody parameters:\n- `templateId` (string) — required — Id or slug of the template to render.\n- `values` (object) — required — Field key → value, matching the template schema from GET /v1/templates/:id. Any {{PLACEHOLDER}} in the template also accepts a value under its own key, whether or not it is a declared field.\n- `output.format` (\"png\" | \"jpeg\" | \"webp\" | \"avif\" | \"pdf\") — Defaults to png. `jpg` is accepted as an alias for `jpeg`. WebP and AVIF are re-encoded from a lossless capture, so `quality` genuinely applies to them.\n- `output.scale` (number) — 1–4. Multiplies the template’s pixel dimensions. Your plan sets the ceiling.\n- `output.quality` (number) — 1–100. Applies to jpeg, webp and avif; ignored by png and pdf.\n- `output.transparent` (boolean) — Drop the canvas background so the output has a real alpha channel. png, webp and avif only — jpeg and pdf have nowhere to put it."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/render"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Render complete\",\n  \"data\": {\n    \"id\": \"rnd_8f2a1c\",\n    \"status\": \"done\",\n    \"kind\": \"image\",\n    \"credits\": 1,\n    \"url\": \"https://cdn.pixbix.app/render/org_4c1d/rnd_8f2a1c/render.png\",\n    \"width\": 1080,\n    \"height\": 1920,\n    \"extracted\": {\n      \"headline\": \"Diwali Sale — 50% off everything\",\n      \"price\": \"₹1,299\"\n    },\n    \"created\": \"2026-08-13T09:14:22.104Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Render a batch",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"defaults\": {\n    \"templateId\": \"diwali-sale-story\",\n    \"output\": {\n      \"format\": \"png\",\n      \"scale\": 2\n    }\n  },\n  \"items\": [\n    {\n      \"values\": {\n        \"headline\": \"Diwali Sale\",\n        \"offer\": \"50% OFF\"\n      },\n      \"idempotencyKey\": \"row-1\"\n    },\n    {\n      \"values\": {\n        \"headline\": \"Year End\",\n        \"offer\": \"30% OFF\"\n      },\n      \"idempotencyKey\": \"row-2\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/render/batch",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "render",
                "batch"
              ]
            },
            "description": "Runs up to 25 renders in one call and returns a row per render. Each row is an ordinary render request — a template and its values, or a raw edit — merged over an optional `defaults` object, so a batch of one template with different values per row need not repeat the template id. ONE ROW NEVER FAILS THE BATCH: rows report their own success or error and the response is 200 either way, so a bad value in row 12 does not throw away the eleven renders already produced. Image rows come back finished, with their URL; video rows come back queued, to be polled or waited for on a webhook exactly as a single render is. No row is ever refused for being busy: your plan’s concurrent-render allowance decides how many run at once, and the rest queue.\n\nRequired scope: `renders:write`\nCost: Charged per row, at the same rate as a single render. A batch that runs out of credit part-way keeps what it paid for and reports INSUFFICIENT_CREDITS on the rest.\n\nBody parameters:\n- `items` (object[]) — required — The renders, 1–25 of them. Each takes the same fields as POST /v1/render: templateId, values, edit, output, callback. A row may also carry its own `idempotencyKey`, which is what makes re-running a partly failed batch safe.\n- `defaults` (object) — Merged UNDER every row, so a row can override any of it. The usual use is one templateId and one output block for the whole batch.\n- `callback` (string) — Webhook for every row that does not name its own. Video rows fire it when they finish encoding."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/render/batch"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Batch complete\",\n  \"data\": {\n    \"total\": 2,\n    \"succeeded\": 2,\n    \"failed\": 0,\n    \"results\": [\n      {\n        \"index\": 0,\n        \"success\": true,\n        \"queued\": false,\n        \"render\": {\n          \"id\": \"rnd_8f2a1c\",\n          \"status\": \"done\",\n          \"kind\": \"image\",\n          \"credits\": 1,\n          \"url\": \"https://cdn.pixbix.app/render/org_4c1d/rnd_8f2a1c/render.png\"\n        }\n      },\n      {\n        \"index\": 1,\n        \"success\": true,\n        \"queued\": false,\n        \"render\": {\n          \"id\": \"rnd_8f2a1d\",\n          \"status\": \"done\",\n          \"kind\": \"image\",\n          \"credits\": 1,\n          \"url\": \"https://cdn.pixbix.app/render/org_4c1d/rnd_8f2a1d/render.png\"\n        }\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Estimate a render",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"templateId\": \"diwali-sale-story\",\n  \"values\": {\n    \"headline\": \"Diwali Sale\",\n    \"offer\": \"50% OFF\"\n  },\n  \"output\": {\n    \"format\": \"mp4\",\n    \"resolution\": \"fhd\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/render/estimate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "render",
                "estimate"
              ]
            },
            "description": "Prices a render without running it. Use this to show a cost before committing, or to check a payload validates.\n\nRequired scope: `renders:read`\n\nBody parameters:\n- `templateId` (string) — required — Id or slug of the template to render.\n- `values` (object) — required — Field key → value, matching the template schema from GET /v1/templates/:id. Any {{PLACEHOLDER}} in the template also accepts a value under its own key, whether or not it is a declared field."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/render/estimate"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"kind\": \"video\",\n    \"credits\": 6,\n    \"durationSeconds\": 12,\n    \"estimatedSeconds\": 45\n  }\n}"
            }
          ]
        },
        {
          "name": "List renders",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/render",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "render"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "Page number, from 1.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Rows per page, 1–100. Defaults to 20.",
                  "disabled": true
                },
                {
                  "key": "sort",
                  "value": "createdAt",
                  "description": "Column to order by: createdAt, finishedAt, status, kind, renderMs, creditCost. Defaults to `createdAt`.",
                  "disabled": true
                },
                {
                  "key": "order",
                  "value": "desc",
                  "description": "Sort direction. Defaults to `desc` for dates and counts.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "diwali",
                  "description": "Case-insensitive match across the render id, the engine job id. Up to 128 characters.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "done",
                  "description": "queued, fetching, rendering, saving, done, failed or cancelled. Comma-separate several to match any of them.",
                  "disabled": true
                },
                {
                  "key": "kind",
                  "value": "image",
                  "description": "Restrict to one engine.",
                  "disabled": true
                },
                {
                  "key": "templateId",
                  "value": "diwali-sale-story",
                  "description": "Only renders from this template.",
                  "disabled": true
                },
                {
                  "key": "createdFrom",
                  "value": "2026-08-01",
                  "description": "ISO date. Only renders queued on or after it.",
                  "disabled": true
                },
                {
                  "key": "createdTo",
                  "value": "2026-08-31",
                  "description": "ISO date. Only renders queued on or before it.",
                  "disabled": true
                }
              ]
            },
            "description": "Most recent first. Covers both image and video renders.\n\nRequired scope: `renders:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/render"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"rnd_8f2a1c\",\n      \"status\": \"done\",\n      \"kind\": \"image\",\n      \"credits\": 1\n    }\n  ],\n  \"pagination\": {\n    \"page\": 1,\n    \"limit\": 20,\n    \"total\": 1,\n    \"totalPages\": 1,\n    \"hasMore\": false,\n    \"sort\": \"createdAt\",\n    \"order\": \"desc\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Get a render",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/render/{{renderId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "render",
                "{{renderId}}"
              ]
            },
            "description": "Fetch one render by id. This is the endpoint to poll when you are not using webhooks. A finished render may carry `warnings`: the output exists and follows the edit exactly, but something in the edit probably does not say what you meant. It carries `extracted` when the template photographed a live page and read values off it — the same values a `render.completed` webhook delivers, kept on the render so they are still there whenever you come back for them.\n\n`format` and `codec` describe what was DELIVERED, which is not always what was asked for: some codecs cannot be muxed into some containers, so a request for ProRes in MP4 comes back as MOV and a transparent MP4 comes back as WebM. A `warnings` entry says so when it happens. Read `format` rather than the format you requested when naming the file or choosing a player — a MOV carrying ProRes is an editing master and no browser will play it.\n\nRequired scope: `renders:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/render/{{renderId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"rnd_8f2a1c\",\n    \"status\": \"done\",\n    \"progress\": 100,\n    \"url\": \"https://cdn.pixbix.app/render/org_4c1d/rnd_8f2a1c/render.png\",\n    \"format\": \"png\",\n    \"codec\": null,\n    \"warnings\": [],\n    \"extracted\": {\n      \"headline\": \"Diwali Sale — 50% off everything\",\n      \"price\": \"₹1,299\"\n    }\n  }\n}"
            }
          ]
        },
        {
          "name": "Cancel a render",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/render/{{renderId}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "render",
                "{{renderId}}",
                "cancel"
              ]
            },
            "description": "Stops a queued or in-flight render. Credits for a cancelled render are refunded.\n\nRequired scope: `renders:write`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/render/{{renderId}}/cancel"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"rnd_8f2a1c\",\n    \"status\": \"cancelled\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Generate from a template",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"templateId\": \"tpl_7d21\",\n  \"fields\": {\n    \"headline\": \"Season sale\",\n    \"price\": \"₹1,499\"\n  },\n  \"format\": \"png\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/generate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "generate"
              ]
            },
            "description": "Fills a template's fields and returns a finished image. The template-driven counterpart to /v1/render: send field values by key rather than a full design. Accepts JSON, or multipart when a field takes an uploaded file — use the field key as the file's form name. Templates you can reach are your own plus anything published to the public library.\n\nRequired scope: `renders:write`\nCost: 1 credit per generated image. Test keys render watermarked and cost nothing.\n\nBody parameters:\n- `templateId` (string) — required — Template to render.\n- `fields` (object) — Field values keyed by the template's field keys. May also be a JSON string. Keys matching a field can be sent at the top level instead.\n- `format` (string) — png, jpeg, webp or pdf. Pass `binary` to receive image bytes rather than a hosted URL."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/generate"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Graphic generated\",\n  \"data\": {\n    \"url\": \"https://cdn.pixbix.app/render/org_4c1d/rnd_8f2a/render.png\",\n    \"width\": 1080,\n    \"height\": 1080,\n    \"format\": \"png\"\n  }\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Video API",
      "description": "Render a timeline to MP4, WebM, MOV or GIF. Video is queued and reported by webhook.",
      "item": [
        {
          "name": "Render a video",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"templateId\": \"diwali-sale-story\",\n  \"values\": {\n    \"headline\": \"Diwali Sale\",\n    \"offer\": \"50% OFF\",\n    \"product\": \"https://cdn.acme.com/lamp.jpg\"\n  },\n  \"output\": {\n    \"format\": \"mp4\",\n    \"resolution\": \"fhd\",\n    \"fps\": 30\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/render",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "render"
              ]
            },
            "description": "The same endpoint as an image render — the template’s kind decides the engine. Video is queued: the response returns immediately with a job id, and completion arrives by webhook or by polling GET /v1/render/:id. Submit as many as you like — your plan’s concurrent-render allowance controls how many run at once, not how many you may send. Anything over it waits at `queued` and starts automatically as slots free, with `stage` saying so.\n\nRequired scope: `renders:write`\nCost: Video credits scale with duration and resolution. Price it first with /v1/render/estimate.\n\nBody parameters:\n- `templateId` (string) — required — Id or slug of the template to render.\n- `values` (object) — required — Field key → value, matching the template schema from GET /v1/templates/:id. Any {{PLACEHOLDER}} in the template also accepts a value under its own key, whether or not it is a declared field.\n- `output.format` (\"mp4\" | \"webm\" | \"mov\" | \"gif\") — Defaults to mp4.\n- `output.resolution` (\"sd\" | \"hd\" | \"fhd\" | \"4k\") — Defaults to hd.\n- `output.fps` (number) — 24, 30 or 60.\n- `webhookUrl` (string) — Overrides the workspace webhook for this render only."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/render"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Render queued\",\n  \"data\": {\n    \"id\": \"rnd_8f2a1c\",\n    \"status\": \"queued\",\n    \"progress\": 0,\n    \"kind\": \"video\",\n    \"stage\": \"Queued — your plan renders 3 at a time and 3 are already running.\",\n    \"credits\": 6,\n    \"created\": \"2026-08-13T09:14:22.104Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Render an edit directly",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"timeline\": {\n    \"background\": \"#000000\",\n    \"tracks\": [\n      {\n        \"name\": \"Type\",\n        \"clips\": [\n          {\n            \"asset\": {\n              \"type\": \"text\",\n              \"text\": \"Diwali Sale\",\n              \"font\": {\n                \"family\": \"Inter\",\n                \"size\": 72,\n                \"weight\": 700,\n                \"color\": \"#FFFFFF\"\n              },\n              \"alignment\": {\n                \"horizontal\": \"center\",\n                \"vertical\": \"center\"\n              }\n            },\n            \"start\": 0,\n            \"length\": 3,\n            \"transition\": {\n              \"in\": \"fade\",\n              \"out\": \"fade\"\n            }\n          }\n        ]\n      },\n      {\n        \"name\": \"Countdown\",\n        \"clips\": [\n          {\n            \"asset\": {\n              \"type\": \"countdown\",\n              \"to\": \"2026-12-25T09:00:00Z\",\n              \"layout\": \"segments\",\n              \"units\": [\n                \"days\",\n                \"hours\",\n                \"minutes\",\n                \"seconds\"\n              ],\n              \"labels\": {\n                \"show\": true,\n                \"style\": \"long\",\n                \"position\": \"below\"\n              },\n              \"segment\": {\n                \"background\": \"rgba(255,255,255,0.10)\",\n                \"borderRadius\": 16,\n                \"gap\": 18\n              },\n              \"urgency\": [\n                {\n                  \"belowSec\": 3600,\n                  \"color\": \"#FF6D00\"\n                }\n              ],\n              \"expired\": {\n                \"text\": \"Sale closed\"\n              },\n              \"font\": {\n                \"family\": \"Inter\",\n                \"size\": 84,\n                \"weight\": 800,\n                \"color\": \"#FFFFFF\"\n              }\n            },\n            \"start\": 0,\n            \"length\": 3,\n            \"position\": \"center\"\n          }\n        ]\n      },\n      {\n        \"name\": \"Numbers\",\n        \"clips\": [\n          {\n            \"asset\": {\n              \"type\": \"chart\",\n              \"chart\": \"bar\",\n              \"title\": \"Enquiries by month\",\n              \"data\": {\n                \"labels\": [\n                  \"Jun\",\n                  \"Jul\",\n                  \"Aug\",\n                  \"Sep\"\n                ],\n                \"series\": [\n                  {\n                    \"name\": \"Rentals\",\n                    \"values\": [\n                      18,\n                      34,\n                      27,\n                      41\n                    ]\n                  },\n                  {\n                    \"name\": \"Sales\",\n                    \"values\": [\n                      8,\n                      11,\n                      9,\n                      14\n                    ]\n                  }\n                ]\n              },\n              \"showLegend\": true,\n              \"showValues\": true,\n              \"font\": {\n                \"family\": \"Inter\",\n                \"color\": \"#E2E8F0\"\n              },\n              \"fontScale\": 1.5,\n              \"gridColor\": \"#334155\",\n              \"text\": {\n                \"title\": {\n                  \"align\": \"left\",\n                  \"scale\": 1.2\n                },\n                \"values\": {\n                  \"weight\": 700\n                },\n                \"labels\": {\n                  \"transform\": \"uppercase\",\n                  \"letterSpacing\": 1\n                },\n                \"axis\": {\n                  \"color\": \"#94A3B8\",\n                  \"scale\": 0.9\n                }\n              },\n              \"legend\": {\n                \"position\": \"right\",\n                \"swatch\": \"circle\"\n              },\n              \"padding\": {\n                \"bottom\": 8\n              },\n              \"animation\": {\n                \"type\": \"drawOn\",\n                \"durationSec\": 1.2\n              }\n            },\n            \"start\": 0,\n            \"length\": 3,\n            \"width\": 900,\n            \"height\": 460,\n            \"position\": \"center\"\n          }\n        ]\n      },\n      {\n        \"name\": \"Datestamp\",\n        \"clips\": [\n          {\n            \"asset\": {\n              \"type\": \"datetime\",\n              \"mode\": \"date\",\n              \"value\": \"now\",\n              \"format\": \"long\",\n              \"timezone\": \"Asia/Kolkata\",\n              \"prefix\": \"Offer valid \",\n              \"font\": {\n                \"family\": \"Inter\",\n                \"size\": 36,\n                \"weight\": 600,\n                \"color\": \"#FFFFFF\"\n              }\n            },\n            \"start\": 0,\n            \"length\": 3,\n            \"position\": \"bottom\",\n            \"offset\": {\n              \"y\": 0.06\n            }\n          }\n        ]\n      },\n      {\n        \"name\": \"Presenter\",\n        \"clips\": [\n          {\n            \"asset\": {\n              \"type\": \"video\",\n              \"src\": \"https://cdn.acme.com/presenter-greenscreen.mp4\",\n              \"chromaKey\": {\n                \"color\": \"#00B140\",\n                \"threshold\": 60,\n                \"halo\": 24,\n                \"spill\": 0.6,\n                \"edgeBlur\": 2\n              }\n            },\n            \"start\": 0,\n            \"length\": 3,\n            \"fit\": \"contain\",\n            \"position\": \"bottomRight\",\n            \"scale\": 0.7\n          }\n        ]\n      },\n      {\n        \"name\": \"Shape\",\n        \"useLumaMatte\": true,\n        \"lumaPolarity\": \"dark\",\n        \"clips\": [\n          {\n            \"asset\": {\n              \"type\": \"luma\",\n              \"src\": \"https://cdn.acme.com/mattes/circle.png\",\n              \"fit\": \"cover\"\n            },\n            \"start\": 0,\n            \"length\": 3\n          }\n        ]\n      },\n      {\n        \"name\": \"B-roll\",\n        \"clips\": [\n          {\n            \"asset\": {\n              \"type\": \"video\",\n              \"src\": \"https://cdn.acme.com/sparklers.mp4\",\n              \"trim\": 2,\n              \"speed\": 0.5,\n              \"reverse\": true,\n              \"mute\": true\n            },\n            \"start\": 0,\n            \"length\": 3,\n            \"fit\": \"cover\",\n            \"opacity\": 0.35\n          }\n        ]\n      },\n      {\n        \"name\": \"Background\",\n        \"clips\": [\n          {\n            \"asset\": {\n              \"type\": \"image\",\n              \"src\": \"https://cdn.acme.com/lamp.jpg\"\n            },\n            \"start\": 0,\n            \"length\": 3,\n            \"fit\": \"cover\",\n            \"effect\": \"zoomIn\"\n          }\n        ]\n      }\n    ]\n  },\n  \"output\": {\n    \"format\": \"mp4\",\n    \"resolution\": \"fhd\",\n    \"fps\": 30,\n    \"quality\": \"high\",\n    \"bitrateKbps\": 4500,\n    \"twoPass\": true,\n    \"normalizeAudio\": true\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/render",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "render"
              ]
            },
            "description": "The same endpoint again, given a whole edit instead of a template id. Nothing is looked up and nothing is stored, so this is the form to use when your own system already owns the composition. The video studio’s “Edit spec” export is exactly this body, and the studio imports it back.\n\nRequired scope: `renders:write`\nCost: Priced like any other video render — duration × resolution. Check it with /v1/render/estimate.\n\nBody parameters:\n- `timeline` (object) — required — Tracks of clips, plus optional background, soundtrack and font declarations. Track 0 is the topmost layer. A track or a clip carrying enabled: false is dropped from the render, picture and audio both, without retiming anything around it — clips using start: \"auto\" still chain from where it ends, and the duration is unchanged. Absent means visible.\n- `asset.font (title)` (object) — A title names a look with `style` — Blockbuster is Oswald 700, Vogue is Playfair Display, and so on — and every part of that preset can be overridden: `font` (family, size in pixels, weight, colour, letterSpacing, lineHeight, style), `alignment` for both axes, and `stroke`/`shadow`. A `font.size` in pixels wins over the `size` keyword, which is a proportion of the frame. Anything omitted keeps the preset, so an existing title renders exactly as it did.\n- `clip.fit` (\"cover\" | \"contain\" | \"crop\" | \"none\") — How the asset fills the box its scale and position describe. cover (the default) and crop scale it, aspect preserved, until it COVERS that box — the excess overflows and is cropped by the output frame, not by the box, so a 3:2 photo at scale 0.84 in a 16:9 frame still bleeds top to bottom. contain fits the whole asset inside the box; none leaves it at its native pixel size. Give a clip an explicit width/height or a mask and the box becomes a window instead: the picture is cut to it.\n- `timeline.lumaPolarity` (\"white\" | \"dark\" | \"auto\") — Which way round every luma matte in the edit is read. A matte reveals where it is WHITE and hides where it is black — the convention After Effects, SVG and CSS masks all share. Plenty of template libraries author the opposite, the shape drawn black on a white field, and rendered as written those produce the negative of their own design. Set \"dark\" once and every matte in the edit follows, deterministically, with nothing measured. Omit it and each unstated matte is measured instead — a good guess, but a guess. A track sets its own to override this, and a clip’s invert overrides both.\n- `track.lumaPolarity` (\"white\" | \"dark\" | \"auto\") — The same field per layer, and the fix for an edit that MIXES conventions. Measurement cannot tell a shape that is kept from a shape that is a hole — they are the same picture — so an edit combining a dark-keeps template with correctly authored stock mattes cannot be settled by inference at all. Put each set on its own layer and state it here.\n- `asset.invert (luma)` (boolean | \"auto\") — One matte’s polarity, overriding its track and the timeline. true is dark-keeps, false is white-keeps, \"auto\" measures this matte and decides while ignoring any convention stated further out — which is how you exempt a single asset. Absent inherits.\n- `asset.fit (luma)` (\"stretch\" | \"cover\" | \"contain\") — How the stencil maps onto the clip it masks. stretch (the default) fills that clip’s box exactly, which is right for a matte authored against this design; cover and contain keep the matte’s own aspect, which is what stops a square avatar stencil arriving as an oval on a portrait render. What a contain matte does not reach is KEPT, not cut away.\n- `asset.fontScale (chart)` (number) — Multiplies every piece of type a chart, table or widget draws — the title, the axis ticks, the category labels, the values on the marks, the legend and its swatches, a table’s rows, a widget’s readout. 1 is the chart as drawn. The one-field answer to a chart whose labels are too small, which is what a chart in a vertical frame usually is: it was laid out on a monitor and it is watched on a phone. Start around 1.4–1.8 for 9:16.\n- `asset.text (chart)` (object) — Per-role typography, for when one label has to move and the rest must not. Nine roles: `title`, `axis` (the value-axis ticks), `labels` (the category names), `values` (each datum beside its mark), `legend`, `header` and `cell` for a table, and `value` and `caption` for a widget. Each takes { family, size, scale, weight, color, opacity, letterSpacing, transform, align, offset, hidden }. `size` is absolute and ignores fontScale; `scale` is relative to the role’s own default and composes with it. `hidden` draws nothing AND gives the role’s room back to the plot, which is how a chart becomes a sparkline without a second asset.\n- `asset.legend (chart)` (object) — { position, align, direction, swatch, swatchSize, gap, itemGap, rowGap, size }. `position` is top, bottom, left or right — bottom by default on an axis plot and right on a pie — and the plot gives up the room rather than being drawn over, so moving a legend re-lays the chart around it. `swatch` is square, circle or line. Note this places a legend rather than turning one on: that is showLegend, which needs more than one series on an axis plot.\n- `asset.padding (chart)` (number | object) — EXTRA room around a chart’s plot, as a number or { top, right, bottom, left } — added to what the chart works out for itself rather than replacing it. Additive on purpose: the chart already sizes its left gutter to fit its own tick labels, and a caller who set that outright would have to recompute it every time the data changed. Negative values tighten.\n- `asset.gridColor / axisTicks / valuePosition (chart)` (string | number | string) — The gridline colour behind an axis plot (showGrid still owns whether they are drawn); the number of divisions on the value axis, 4 by default so five labels counting zero; and where showValues puts a bar’s number — \"auto\" keeps it outside unless it would overflow, then inverts it to white inside the bar, while \"inside\" always inverts and \"outside\" never does.\n- `asset.speed / reverse (video, audio)` (number | boolean) — `speed` is the playback rate, 0.25–4, default 1; `reverse` plays the clip’s window backwards. Both retime the picture AND the sound, so a clip at 2 sounds fast rather than merely ending early and a reversed clip’s audio runs backwards with it. The clip’s length does not change — the rate decides how much SOURCE fills the window, so four seconds at 2 reads eight seconds of footage and runs out of picture if the file is shorter. Use \"length\": \"auto\" to take the playable length at the rate you set. Reversal cannot stream — the whole window is decoded and held — so keep reversed clips short.\n- `output` (object) — required — Format, resolution or size, fps, quality and encoding options.\n- `output.bitrateKbps` (number) — Target an average bitrate instead of a constant quality. Leave it off and the encode is CRF, which spends fewer bits for the same picture on the flat colour and text a template is mostly made of — set it only when you need a predictable file size.\n- `output.twoPass` (boolean) — Measure the material first, then spend the bitrate where the first pass found it was needed. It works only alongside bitrateKbps: against CRF there is no fixed budget to redistribute, so a second pass cannot change anything and the render stays single-pass rather than charging you for one. Roughly doubles encode time. Supported on h264, h265 and vp9.\n- `output.normalizeAudio` (boolean) — Normalise the finished mix to −14 LUFS, the level the social platforms target — so a quiet voice-over and a loud music bed arrive at a comparable loudness instead of being re-levelled on upload.\n- `output.destinations` (array) — Extra places to deliver the finished file, on top of pixbix storage: [{ provider, options }]. \"s3\" takes bucket, region, optional endpoint (which is what points it at any S3-compatible service) and credentials; \"http\" PUTs or POSTs the bytes to a signed upload URL, which is how Google Cloud Storage and Azure Blob are written to. A delivery that fails does NOT fail the render — the output still lands in pixbix storage and the reason comes back in the job’s warnings. Set exclude: true to keep a destination configured without using it.\n- `merge` (array) — Placeholder substitutions applied to every string in the edit: [{ find, replace }].\n- `callback` (string) — Overrides the workspace webhook for this render only."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/render"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Render queued\",\n  \"data\": {\n    \"id\": \"rnd_4b71de\",\n    \"status\": \"queued\",\n    \"progress\": 0,\n    \"kind\": \"video\",\n    \"credits\": 3,\n    \"created\": \"2026-08-13T09:14:22.104Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Inspect a media URL",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://cdn.acme.com/clips/diwali-hero.mp4\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/video/info",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "video",
                "info"
              ]
            },
            "description": "Runs ffprobe against a public URL and returns everything it can read — duration, dimensions, frame rate, codecs, bitrate, rotation, audio and subtitle streams, chapters and container metadata — without rendering anything and without spending a credit. Also reports whether the file is usable as a clip source, and what would go wrong if not. The engine reads the container header over ranged HTTP, so a large file normally costs a few hundred kilobytes of transfer rather than a full download. Works on audio and image URLs too, which is how you answer \"is this actually a video?\".\n\nRequired scope: `renders:read`\n\nBody parameters:\n- `url` (string) — required — Public http(s) address of the file. Private, loopback and link-local addresses are refused, and every redirect hop is re-checked before it is followed."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/video/info"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Media inspected\",\n  \"data\": {\n    \"url\": \"https://cdn.acme.com/clips/diwali-hero.mp4\",\n    \"finalUrl\": \"https://cdn.acme.com/clips/diwali-hero.mp4\",\n    \"probedVia\": \"remote\",\n    \"probeMs\": 412,\n    \"cached\": false,\n    \"kind\": \"video\",\n    \"container\": \"mov,mp4,m4a,3gp,3g2,mj2\",\n    \"containerLongName\": \"QuickTime / MOV\",\n    \"mimeType\": \"video/mp4\",\n    \"sizeBytes\": 18446221,\n    \"durationSec\": 32.533,\n    \"duration\": \"00:00:32.533\",\n    \"bitrateKbps\": 4536,\n    \"startTimeSec\": 0,\n    \"streamCount\": 2,\n    \"hasVideo\": true,\n    \"hasAudio\": true,\n    \"hasAlpha\": false,\n    \"fastStart\": true,\n    \"video\": {\n      \"index\": 0,\n      \"codec\": \"h264\",\n      \"codecLongName\": \"H.264 / AVC / MPEG-4 AVC\",\n      \"profile\": \"High\",\n      \"level\": 40,\n      \"width\": 1920,\n      \"height\": 1080,\n      \"displayWidth\": 1920,\n      \"displayHeight\": 1080,\n      \"aspectRatio\": \"16:9\",\n      \"sampleAspectRatio\": \"1:1\",\n      \"orientation\": \"landscape\",\n      \"pixelFormat\": \"yuv420p\",\n      \"fps\": 29.97,\n      \"nominalFps\": 29.97,\n      \"frameCount\": 975,\n      \"bitrateKbps\": 4352,\n      \"rotation\": 0,\n      \"hasAlpha\": false,\n      \"isHdr\": false,\n      \"colorSpace\": \"bt709\",\n      \"colorPrimaries\": \"bt709\",\n      \"colorTransfer\": \"bt709\",\n      \"colorRange\": \"tv\",\n      \"language\": \"und\"\n    },\n    \"audio\": [\n      {\n        \"index\": 1,\n        \"codec\": \"aac\",\n        \"codecLongName\": \"AAC (Advanced Audio Coding)\",\n        \"profile\": \"LC\",\n        \"channels\": 2,\n        \"channelLayout\": \"stereo\",\n        \"sampleRateHz\": 48000,\n        \"bitrateKbps\": 184,\n        \"durationSec\": 32.512,\n        \"language\": \"eng\",\n        \"title\": null\n      }\n    ],\n    \"subtitles\": [],\n    \"chapters\": [],\n    \"tags\": {\n      \"major_brand\": \"isom\",\n      \"encoder\": \"Lavf60.16.100\",\n      \"creation_time\": \"2026-08-02T11:41:08.000000Z\"\n    },\n    \"usable\": true,\n    \"warnings\": [],\n    \"limits\": {\n      \"maxDurationSec\": 1800,\n      \"maxWidth\": 3840,\n      \"maxHeight\": 3840,\n      \"maxFps\": 60,\n      \"maxSizeBytes\": 536870912\n    }\n  }\n}"
            }
          ]
        },
        {
          "name": "Inspect a media URL (GET)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/video/info",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "video",
                "info"
              ],
              "query": [
                {
                  "key": "url",
                  "value": "https://cdn.acme.com/clips/diwali-hero.mp4",
                  "description": "Public http(s) address of the file, URL-encoded.",
                  "disabled": false
                }
              ]
            },
            "description": "The same probe as the POST above, with the URL as a query parameter — a read with one argument, so it pastes into a browser or a curl one-liner. Identical response, identical metering.\n\nRequired scope: `renders:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/video/info"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Media inspected\",\n  \"data\": {\n    \"kind\": \"video\",\n    \"durationSec\": 32.533,\n    \"duration\": \"00:00:32.533\",\n    \"sizeBytes\": 18446221,\n    \"hasVideo\": true,\n    \"hasAudio\": true,\n    \"video\": {\n      \"codec\": \"h264\",\n      \"displayWidth\": 1920,\n      \"displayHeight\": 1080,\n      \"fps\": 29.97,\n      \"orientation\": \"landscape\"\n    },\n    \"usable\": true,\n    \"warnings\": []\n  }\n}"
            }
          ]
        },
        {
          "name": "List available fonts",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/fonts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "fonts"
              ],
              "query": [
                {
                  "key": "family",
                  "value": "Playfair Display",
                  "description": "Check one exact name. Returns { supported, font } instead of a list. Case- and whitespace-insensitive.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "mono",
                  "description": "Substring match on the family name.",
                  "disabled": true
                },
                {
                  "key": "category",
                  "value": "serif",
                  "description": "Restrict to one category.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "100",
                  "description": "1–2000. Defaults to 100.",
                  "disabled": true
                }
              ]
            },
            "description": "Every font family the render engine can serve, most popular first, with the weights each one ships. Open — it needs no API key. Pass ?family= to check one exact name before you spend a render on it: Google answers a request for a family it does not have with a silent fallback, so a typo produces a finished video in the wrong typeface and no error anywhere.\n\nNo scope required."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/fonts"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"total\": 1942,\n    \"matched\": 349,\n    \"returned\": 100,\n    \"categories\": [\n      \"sans-serif\",\n      \"serif\",\n      \"display\",\n      \"handwriting\",\n      \"monospace\"\n    ],\n    \"generics\": [\n      \"sans-serif\",\n      \"serif\",\n      \"monospace\",\n      \"system-ui\",\n      \"cursive\"\n    ],\n    \"fonts\": [\n      {\n        \"family\": \"Playfair Display\",\n        \"category\": \"serif\",\n        \"weights\": [\n          400,\n          500,\n          600,\n          700,\n          800,\n          900\n        ],\n        \"italic\": true,\n        \"variable\": true\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Font categories",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/fonts/categories",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "fonts",
                "categories"
              ]
            },
            "description": "How many families sit in each category — enough to build a picker’s filter bar without pulling two thousand records to count them.\n\nNo scope required."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/fonts/categories"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"total\": 1942,\n    \"categories\": {\n      \"sans-serif\": 717,\n      \"serif\": 349,\n      \"display\": 467,\n      \"handwriting\": 358,\n      \"monospace\": 51\n    },\n    \"generics\": [\n      \"sans-serif\",\n      \"serif\",\n      \"monospace\",\n      \"system-ui\",\n      \"cursive\"\n    ]\n  }\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Screenshot API",
      "description": "Capture any URL or raw HTML in real headless Chrome.",
      "item": [
        {
          "name": "Capture a screenshot",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://example.com\",\n  \"format\": \"png\",\n  \"fullPage\": true,\n  \"viewport\": {\n    \"width\": 1440,\n    \"height\": 900\n  },\n  \"extract\": {\n    \"headline\": \"h1\",\n    \"price\": \".price\",\n    \"canonical\": \"link[rel=canonical]\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/screenshot",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screenshot"
              ]
            },
            "description": "Renders any URL — or a raw HTML string — in real headless Chrome and returns an image or PDF. Real web fonts, CSS grid, canvas and lazy-loaded images all behave as they do in a browser.\n\nNo scope required.\nCost: 1 image credit per capture.\n\nBody parameters:\n- `url` (string) — Page to capture. Provide either url or html.\n- `html` (string) — Raw HTML to render instead of fetching a URL.\n- `presetId` (string) — Apply a saved preset as the baseline. Anything else you send overrides it, field by field — nested objects merge, so setting viewport.width keeps the preset's height.\n- `format` (\"png\" | \"jpeg\" | \"webp\" | \"avif\" | \"pdf\") — Defaults to png.\n- `fullPage` (boolean) — Capture the entire scroll height.\n- `viewport.width` (number) — Defaults to 1280.\n- `viewport.height` (number) — Defaults to 800.\n- `selector` (string) — Capture only the first element matching this CSS selector.\n- `waitUntil` (string) — load, domcontentloaded, networkidle0 or networkidle2.\n- `cookies` (array) — Cookies to set before navigating, for pages behind a login: [{ name, value, domain?, path?, httpOnly?, secure?, expires? }]. domain defaults to the host of url.\n- `authenticate` (object) — HTTP basic auth: { username, password }.\n- `response` (\"url\" | \"async\" | \"binary\" | \"base64\" | \"json\") — How you want the capture back. Defaults to url — the image is stored and you get a link. binary returns the raw bytes, base64 returns them inline, json returns the metadata with no image. async does not wait: you get 202 with a job id straight away, and poll GET /v1/renders/:id until it reports done. Use it whenever you are not showing the picture to somebody who is waiting for it — a capture is at the mercy of the page it is capturing, and a slow site should cost you a poll rather than a held-open request and a client timeout.\n- `extract` (array | object) — Values to read off the page alongside the picture, returned under `extracted`. Either an array of { key, selector, attribute?, all?, index?, layerId?, layerField? } or the shorthand { key: selector }. `attribute` reads an attribute instead of the visible text (href, src, content); `all` returns every match as an array; `index` reads one specific match, counting from 0, for the page where the value you want is the third `.price` on it. `all` and `index` cannot be combined. In an image template, `layerId` points the value at a layer — text takes it as words, an image layer as its picture, and a chart, table, widget or code layer as data, with `layerField` naming the slot (`values`, `labels`, `title`, `column:0`, `value`, `label`, `suffix`, `max`) and an optional position filling one entry of it (`values:2`, `column:1:4`). A chart of several series addresses one of them as `series:2`, or one entry of it as `series:2:0`. Several extractions may feed one element — each writes only the slot it names, and they compose, so three reads fill three bars of the same chart. A position past the end grows the element to reach it. Read in the same navigation as the capture, so the values always describe the image beside them. Up to 25 per request."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/screenshot"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Screenshot captured\",\n  \"data\": {\n    \"url\": \"https://cdn.pixbix.app/render/org_4c1d/cap_6f0e2a/homepage.png\",\n    \"format\": \"png\",\n    \"width\": 1440,\n    \"height\": 3180,\n    \"bytes\": 284113,\n    \"cached\": false,\n    \"extracted\": {\n      \"headline\": \"The fastest way to ship\",\n      \"price\": \"₹4,999\",\n      \"canonical\": null\n    }\n  }\n}"
            }
          ]
        },
        {
          "name": "Capture via query string",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screenshot",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screenshot"
              ],
              "query": [
                {
                  "key": "presetId",
                  "value": "prs_9d2f",
                  "description": "Apply a saved preset as the baseline. Any other parameter you send overrides it.",
                  "disabled": true
                },
                {
                  "key": "url",
                  "value": "https://example.com",
                  "description": "Page to capture.",
                  "disabled": false
                },
                {
                  "key": "format",
                  "value": "png",
                  "description": "Defaults to png.",
                  "disabled": true
                },
                {
                  "key": "fullPage",
                  "value": "true",
                  "description": "Capture the entire scroll height.",
                  "disabled": true
                }
              ]
            },
            "description": "The same capture expressed as a GET, so it can be dropped straight into an <img> tag or a spreadsheet formula.\n\nNo scope required.\nCost: 1 image credit per capture, unless served from cache."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/screenshot"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"url\": \"https://cdn.pixbix.app/render/org_4c1d/cap_6f0e2a/homepage.png\",\n    \"cached\": true\n  }\n}"
            }
          ]
        },
        {
          "name": "Batch capture",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"presetId\": \"prs_9d2f\",\n  \"items\": [\n    {\n      \"url\": \"https://example.com\"\n    },\n    {\n      \"url\": \"https://example.org\",\n      \"fullPage\": true\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/screenshot/batch",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screenshot",
                "batch"
              ]
            },
            "description": "Up to 20 captures in one request, sharing one browser instance. Each entry takes the same options as a single capture.\n\nNo scope required.\nCost: 1 image credit per capture in the batch.\n\nBody parameters:\n- `items` (object[]) — required — Array of capture option objects.\n- `presetId` (string) — Applies a saved preset to every item in the batch — the usual way to capture many URLs with one shared recipe. An item may carry its own presetId, which wins for that item.\n- `defaults` (object) — Options merged into every item, applied over the preset and under each item’s own options."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/screenshot/batch"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"url\": \"https://cdn.pixbix.app/render/org_4c1d/cap_a1b2/example-com.png\",\n      \"ok\": true\n    },\n    {\n      \"url\": \"https://cdn.pixbix.app/render/org_4c1d/cap_c3d4/example-org.png\",\n      \"ok\": true\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "List saved presets",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screenshot/presets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screenshot",
                "presets"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "Page number, from 1.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "100",
                  "description": "Rows per page, 1–100. Defaults to 100.",
                  "disabled": true
                },
                {
                  "key": "sort",
                  "value": "name",
                  "description": "Column to order by: name, createdAt, updatedAt. Defaults to `name`.",
                  "disabled": true
                },
                {
                  "key": "order",
                  "value": "desc",
                  "description": "Sort direction. Defaults to `desc` for dates and counts.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "diwali",
                  "description": "Case-insensitive match across name. Up to 128 characters.",
                  "disabled": true
                }
              ]
            },
            "description": "Every capture preset saved in this workspace, by name. Use an id as `presetId` on a capture to apply the whole saved option set.\n\nNo scope required."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/screenshot/presets"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"prs_9d2f\",\n      \"name\": \"Pricing page — retina\",\n      \"options\": {\n        \"url\": \"https://acme.in/pricing\",\n        \"format\": \"png\",\n        \"fullPage\": true,\n        \"scale\": 2,\n        \"viewport\": {\n          \"width\": 1440,\n          \"height\": 900\n        },\n        \"blockCookieBanners\": true,\n        \"hideSelectors\": [\n          \"#intercom-container\"\n        ]\n      },\n      \"updatedAt\": \"2026-08-14T11:02:19.441Z\"\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Get a preset",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screenshot/presets/{{presetId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screenshot",
                "presets",
                "{{presetId}}"
              ]
            },
            "description": "One preset and the exact options it holds — useful for inspecting what a `presetId` will apply before you rely on it.\n\nNo scope required."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/screenshot/presets/{{presetId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"prs_9d2f\",\n    \"name\": \"Pricing page — retina\",\n    \"options\": {\n      \"url\": \"https://acme.in/pricing\",\n      \"format\": \"png\",\n      \"fullPage\": true,\n      \"scale\": 2\n    },\n    \"createdAt\": \"2026-08-01T08:15:00.000Z\",\n    \"updatedAt\": \"2026-08-14T11:02:19.441Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Save a preset",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Pricing page — retina\",\n  \"options\": {\n    \"url\": \"https://acme.in/pricing\",\n    \"format\": \"png\",\n    \"fullPage\": true,\n    \"scale\": 2,\n    \"viewport\": {\n      \"width\": 1440,\n      \"height\": 900\n    },\n    \"blockCookieBanners\": true,\n    \"hideSelectors\": [\n      \"#intercom-container\"\n    ]\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/screenshot/presets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screenshot",
                "presets"
              ]
            },
            "description": "Creates a preset, or updates the existing one with the same name — names are unique per workspace, so saving twice edits rather than duplicating. Options are validated before they are stored, so a preset that cannot be captured is rejected here rather than failing later on reuse.\n\nNo scope required.\n\nBody parameters:\n- `name` (string) — required — Up to 60 characters. Unique within the workspace.\n- `options` (object) — required — Any capture options, exactly as POST /v1/screenshot accepts them. Delivery fields (response, download, filename) are stripped — those are decided per capture, not saved."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/screenshot/presets"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Preset saved\",\n  \"data\": {\n    \"id\": \"prs_9d2f\",\n    \"name\": \"Pricing page — retina\",\n    \"options\": {\n      \"url\": \"https://acme.in/pricing\"\n    }\n  }\n}"
            }
          ]
        },
        {
          "name": "Delete a preset",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screenshot/presets/{{presetId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screenshot",
                "presets",
                "{{presetId}}"
              ]
            },
            "description": "Removes a saved preset. Captures already taken are unaffected; jobs still naming this id will start failing with PRESET_NOT_FOUND.\n\nNo scope required."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/screenshot/presets/{{presetId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Preset deleted\",\n  \"data\": {\n    \"id\": \"prs_9d2f\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Options reference",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screenshot/options",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screenshot",
                "options"
              ]
            },
            "description": "Returns the full, live option set this deployment accepts, as JSON. Open — it needs no API key, so it is safe to call from anywhere.\n\nNo scope required."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/screenshot/options"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"formats\": [\n      \"png\",\n      \"jpeg\",\n      \"webp\",\n      \"avif\",\n      \"pdf\"\n    ],\n    \"maxBatch\": 20\n  }\n}"
            }
          ]
        },
        {
          "name": "Renderer status",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screenshot/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screenshot",
                "status"
              ]
            },
            "description": "Health of the render pool: Chromium shards, pages, queue depth and the caches in front of them. `renderCaches.render.hitRate` is the share of image renders served without any compute at all — the number to watch when sizing capacity, and a sudden drop usually means templates changed rather than that traffic grew. `renderQueue.role` says whether this replica renders or dispatches; a non-zero `renderQueue.breakerOpenMs` means it cannot reach the queue and is rendering locally, `fallbacks` counts how often that has happened, and `renderQueue.lastError` gives the reason Redis refused — the fastest way to tell a wrong password from an unreachable host. Template renders, URL captures and design imports all go through that queue, so on a dispatching replica `renderer.connected` stays false — no browser is ever launched on the tier serving your users.\n\nNo scope required."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/screenshot/status"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"renderer\": {\n      \"connected\": true,\n      \"remote\": false,\n      \"shards\": 2,\n      \"shardsUp\": 2,\n      \"active\": 3,\n      \"queued\": 0,\n      \"peakQueued\": 12,\n      \"shed\": 0,\n      \"maxConcurrency\": 8,\n      \"queueTimeoutMs\": 30000,\n      \"pages\": {\n        \"idle\": 5,\n        \"open\": 8,\n        \"maxUses\": 50,\n        \"poolSize\": 4,\n        \"created\": 9,\n        \"reused\": 1841,\n        \"retired\": 1\n      }\n    },\n    \"cache\": {\n      \"entries\": 12,\n      \"bytes\": 8419320,\n      \"hits\": 96,\n      \"misses\": 41\n    },\n    \"renderQueue\": {\n      \"enabled\": true,\n      \"role\": \"api\",\n      \"name\": \"pixbix-stills\",\n      \"workerRunning\": false,\n      \"concurrency\": 4,\n      \"waitTimeoutMs\": 30000,\n      \"breakerOpenMs\": 0,\n      \"lastError\": null,\n      \"queued\": 18412,\n      \"local\": 0,\n      \"tooLarge\": 3,\n      \"cacheHits\": 11208,\n      \"timeouts\": 0,\n      \"fallbacks\": 0\n    },\n    \"renderCaches\": {\n      \"render\": {\n        \"enabled\": true,\n        \"entries\": 734,\n        \"bytes\": 511300284,\n        \"hitRate\": 0.62,\n        \"hits\": 1204,\n        \"misses\": 734,\n        \"evictions\": 0\n      },\n      \"fonts\": {\n        \"enabled\": true,\n        \"families\": 11,\n        \"bytes\": 24118400,\n        \"hits\": 1932,\n        \"misses\": 11,\n        \"diskHits\": 4,\n        \"failures\": 0\n      },\n      \"assets\": {\n        \"enabled\": true,\n        \"entries\": 218,\n        \"bytes\": 194883072,\n        \"hits\": 1611,\n        \"misses\": 218,\n        \"failures\": 2,\n        \"blocked\": 2\n      }\n    },\n    \"limits\": {\n      \"maxDimension\": 6000,\n      \"maxScale\": 3,\n      \"timeoutMs\": 30000\n    },\n    \"batchMax\": 10,\n    \"storage\": \"s3\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Clear the capture cache",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/screenshot/cache/clear",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "screenshot",
                "cache",
                "clear"
              ]
            },
            "description": "Drops every cached capture so the next request for a URL re-renders it. Use after deploying a page whose screenshots are being served stale.\n\nNo scope required."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/screenshot/cache/clear"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Cache cleared\",\n  \"data\": {\n    \"cleared\": 128\n  }\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Templates",
      "description": "List templates and read the field schema a render must satisfy. A render always names a template your own workspace has published — a public library template is copied in first.",
      "item": [
        {
          "name": "List templates",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/templates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "templates"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "Page number, from 1.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Rows per page, 1–100. Defaults to 20.",
                  "disabled": true
                },
                {
                  "key": "sort",
                  "value": "sortOrder",
                  "description": "Column to order by: sortOrder, createdAt, updatedAt, name, usageCount. Defaults to `sortOrder`.",
                  "disabled": true
                },
                {
                  "key": "order",
                  "value": "desc",
                  "description": "Sort direction. Defaults to `desc` for dates and counts.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "diwali",
                  "description": "Case-insensitive match across name, tags, description. Up to 128 characters.",
                  "disabled": true
                },
                {
                  "key": "scope",
                  "value": "mine",
                  "description": "Your own templates, or only the shared public library. Both by default.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "published",
                  "description": "Filter your own templates by status. Ignored with scope=library, which is published by definition.",
                  "disabled": true
                },
                {
                  "key": "kind",
                  "value": "image",
                  "description": "Restrict to one engine.",
                  "disabled": true
                },
                {
                  "key": "categoryId",
                  "value": "cat_festive",
                  "description": "Only templates in this category.",
                  "disabled": true
                },
                {
                  "key": "subcategoryId",
                  "value": "sub_diwali",
                  "description": "Only templates in this subcategory.",
                  "disabled": true
                },
                {
                  "key": "size",
                  "value": "story",
                  "description": "square, portrait, landscape or story. Comma-separate several.",
                  "disabled": true
                }
              ]
            },
            "description": "Your workspace’s templates, plus the shared public library. Check `canRender` on each row before using it: only your own published templates can be rendered, and a library template must be copied into your workspace first.\n\nRequired scope: `templates:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/templates"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"diwali-sale-story\",\n      \"name\": \"Diwali sale story\",\n      \"kind\": \"image\",\n      \"width\": 1080,\n      \"height\": 1920,\n      \"status\": \"published\",\n      \"visibility\": \"private\",\n      \"isOwn\": true,\n      \"inLibrary\": false,\n      \"canRender\": true\n    }\n  ],\n  \"pagination\": {\n    \"page\": 1,\n    \"limit\": 20,\n    \"total\": 1,\n    \"totalPages\": 1,\n    \"hasMore\": false,\n    \"sort\": \"sortOrder\",\n    \"order\": \"asc\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Get template schema",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/templates/{{templateId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "templates",
                "{{templateId}}"
              ]
            },
            "description": "The field schema a render must satisfy: every key, its type, whether it is required, and any validation attached to it. Read this before building a payload. `canRender` says whether a render would be accepted right now, and `renderBlockedReason` says what to do when it would not.\n\nRequired scope: `templates:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/templates/{{templateId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"diwali-sale-story\",\n    \"kind\": \"image\",\n    \"status\": \"published\",\n    \"visibility\": \"private\",\n    \"canRender\": true,\n    \"renderBlockedReason\": null,\n    \"fields\": [\n      {\n        \"key\": \"headline\",\n        \"type\": \"text\",\n        \"required\": true,\n        \"config\": {\n          \"maxLength\": 42\n        }\n      },\n      {\n        \"key\": \"product\",\n        \"type\": \"image\",\n        \"required\": true\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Copy a template into your workspace",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/templates/{{templateId}}/duplicate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "templates",
                "{{templateId}}",
                "duplicate"
              ]
            },
            "description": "Takes a copy of a template — one from the public library, or one of your own — and puts it in your workspace. This is the ONLY way a library template becomes renderable: you render your copy, never somebody else’s original. The copy arrives as a private draft, owned by you, with its usage and review history cleared; publish it when you are ready. Counts against your plan’s template allowance.\n\nRequired scope: `templates:write`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/templates/{{templateId}}/duplicate"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Added to your templates. Publish it when you are ready to generate from it.\",\n  \"data\": {\n    \"id\": \"tpl_9c41b2\",\n    \"name\": \"Diwali sale story\",\n    \"status\": \"draft\",\n    \"visibility\": \"private\",\n    \"version\": 1\n  }\n}"
            }
          ]
        },
        {
          "name": "Import a design",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://cdn.acme.com/designs/diwali-post.svg\",\n  \"name\": \"Diwali post\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/templates/import",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "templates",
                "import"
              ]
            },
            "description": "Turns a finished design from another tool into a pixbix template. An SVG or a PSD comes back with its text as real text layers and its photos as image layers; a PNG or JPEG becomes the canvas. Creates a private draft — nothing is published and no credits are spent. A design tool’s own JSON export (`design-json`) is accepted from pixbix staff only: it reproduces the layout exactly but references artwork and fonts by id, so every picture arrives empty.\n\nRequired scope: `templates:write`\n\nBody parameters:\n- `file` (file) — Multipart upload, up to 8 MB — 15 MB for a `.psd`, which carries every layer's pixels as well as a composite. `image/svg+xml` and `.psd` recover editable text and image layers; png, jpeg and webp come in flat; pdf, ai and eps are rasterised flat where the server has Ghostscript. Send this OR `url` OR `content`.\n- `url` (string) — https URL of an SVG or image to import instead of uploading one.\n- `content` (string) — Inline SVG markup, or an HTML fragment copied out of a design tool.\n- `source` (\"svg\" | \"image\" | \"html\" | \"design-json\") — Only needed with `content`, and only when the markup is ambiguous. Inferred otherwise.\n- `fonts` (object) — Font id → family name, for a `design-json` import only — that format identifies typefaces by internal id, so anything unmapped is set in Inter. Ignored by every other format. Staff only, like the format itself.\n- `name` (string) — Template name. Defaults to “Imported design”.\n- `width` (number) — Force a canvas size. Everything is scaled into it. Defaults to the design’s own size.\n- `height` (number) — The other half of `width`."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/templates/import"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Design imported\",\n  \"data\": {\n    \"id\": \"tpl_9a2f4c\",\n    \"name\": \"Diwali post\",\n    \"kind\": \"image\",\n    \"status\": \"draft\",\n    \"visibility\": \"private\",\n    \"width\": 1080,\n    \"height\": 1350,\n    \"import\": {\n      \"format\": \"svg\",\n      \"textLayers\": 4,\n      \"imageLayers\": 1,\n      \"fonts\": [\n        {\n          \"requested\": \"Gilroy-Bold\",\n          \"resolved\": \"Poppins\",\n          \"substituted\": true\n        }\n      ],\n      \"warnings\": [\n        \"1 font had no match and was substituted: Gilroy-Bold → Poppins.\"\n      ]\n    }\n  }\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Assets",
      "description": "Upload and manage the media a template draws from.",
      "item": [
        {
          "name": "List caption providers",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/captions/providers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "captions",
                "providers"
              ]
            },
            "description": "Every speech provider, with your workspace’s connection state on each, plus what the transcript cache has saved you so far. Start here — it tells you which provider will be used if you name none, and which ones can read video.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/captions/providers"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"2 caption providers connected\",\n  \"data\": {\n    \"providers\": [\n      {\n        \"slug\": \"deepgram\",\n        \"name\": \"Deepgram\",\n        \"connected\": true,\n        \"account\": \"Acme Media\",\n        \"recommended\": true,\n        \"support\": {\n          \"video\": true,\n          \"autoLanguage\": true,\n          \"multiLanguage\": true,\n          \"diarize\": true,\n          \"keywords\": true,\n          \"profanityFilter\": true,\n          \"maxBytes\": 524288000\n        }\n      },\n      {\n        \"slug\": \"aws-transcribe\",\n        \"name\": \"Amazon Transcribe\",\n        \"connected\": true,\n        \"account\": \"acme-transcribe · ap-south-1\",\n        \"recommended\": false\n      },\n      {\n        \"slug\": \"google-speech\",\n        \"name\": \"Google Speech-to-Text\",\n        \"connected\": false,\n        \"account\": null,\n        \"recommended\": false\n      }\n    ],\n    \"connected\": 2,\n    \"cache\": {\n      \"transcribed\": 41,\n      \"reused\": 386,\n      \"secondsSaved\": 9142,\n      \"hitRate\": 0.904\n    }\n  }\n}"
            }
          ]
        },
        {
          "name": "Get the default provider’s options",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/captions/options",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "captions",
                "options"
              ]
            },
            "description": "The languages, models and capabilities of whichever provider would be used if you named none. The call to make when you do not want to choose a provider at all — the language codes it returns are the ones your requests should send.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/captions/options"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"provider\": \"deepgram\",\n    \"name\": \"Deepgram\",\n    \"support\": {\n      \"video\": true,\n      \"autoLanguage\": true,\n      \"multiLanguage\": true,\n      \"diarize\": true,\n      \"keywords\": true,\n      \"profanityFilter\": true,\n      \"maxBytes\": 524288000\n    },\n    \"languages\": [\n      {\n        \"code\": \"auto\",\n        \"label\": \"Detect automatically\"\n      },\n      {\n        \"code\": \"en-IN\",\n        \"label\": \"English (India)\"\n      }\n    ],\n    \"models\": [\n      {\n        \"id\": \"nova-3\",\n        \"label\": \"Nova 3\",\n        \"hint\": \"The default. Best accuracy, real-world audio, and the fastest of the three.\"\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Get a provider’s options",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/captions/options/{{provider}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "captions",
                "options",
                "{{provider}}"
              ]
            },
            "description": "The languages, models and capabilities of one provider. Worth reading rather than assuming: the three providers do not use the same language codes — Deepgram takes `hi`, Amazon insists on `hi-IN`. Omit the provider to get the one that would be chosen for you.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/captions/options/{{provider}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"provider\": \"deepgram\",\n    \"name\": \"Deepgram\",\n    \"support\": {\n      \"video\": true,\n      \"autoLanguage\": true,\n      \"multiLanguage\": true,\n      \"diarize\": true,\n      \"keywords\": true,\n      \"profanityFilter\": true,\n      \"maxBytes\": 524288000\n    },\n    \"languages\": [\n      {\n        \"code\": \"auto\",\n        \"label\": \"Detect automatically\"\n      },\n      {\n        \"code\": \"en-IN\",\n        \"label\": \"English (India)\"\n      }\n    ],\n    \"models\": [\n      {\n        \"id\": \"nova-3\",\n        \"label\": \"Nova 3\",\n        \"hint\": \"The default. Best accuracy, real-world audio, and the fastest of the three.\"\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Generate captions",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"assetId\": \"ast_7f21\",\n  \"provider\": \"deepgram\",\n  \"languageCode\": \"en-IN\",\n  \"wordsPerCue\": 5,\n  \"diarize\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/captions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "captions"
              ]
            },
            "description": "Transcribes a recording — audio or video — into word-timed cues, and writes WebVTT and SRT to your library. Returns 202 with a job to poll, or 200 with the finished transcription when the same recording has been transcribed before, in which case nothing is billed. `POST /v1/transcriptions` is the same endpoint under its older name.\n\nRequired scope: `assets:write`\nCost: No render credits. The provider bills your own account per audio-minute — and not at all on a cache hit.\n\nBody parameters:\n- `audioUrl` (string) — URL of the recording. Audio or video. One of this, `assetId` or `renderId` is required.\n- `assetId` (string) — An audio or video asset in your library, instead of a URL. The fastest route to a cache hit — the library already knows the file’s checksum, so a repeat needs no download at all.\n- `renderId` (string) — A finished render, instead of a URL — transcribes its audio track.\n- `provider` (string) — Which speech provider. Omit it and the best connected one is chosen — Deepgram, then Amazon, then Google. Naming a provider that cannot read your file returns 422 saying which ones can.\n- `languageCode` (string) — BCP-47 tag, or `auto` to detect. Codes differ per provider — see `/v1/captions/options`.\n- `alternativeLanguageCodes` (string[]) — Other languages the speaker may switch to mid-recording.\n- `model` (string) — Provider model id. Omit it for the provider’s own default.\n- `diarize` (boolean) — Label who is speaking. Deepgram and Amazon only.\n- `keywords` (string[]) — Names, jargon and product words to bias the model towards. Deepgram only.\n- `wordsPerCue` (number) — Words per caption. Changing this on a repeat is free — the cache re-cuts the stored word timings rather than transcribing again.\n- `charsPerCue` (number) — Characters per caption. The limit that actually governs on a phone. Defaults to 42.\n- `maxCueSec` (number) — Never hold one caption longer than this. Defaults to 6.\n- `punctuation` (boolean) — Ask the recogniser to punctuate. Defaults to on — captions without it read badly, and sentence ends are what cues are broken on.\n- `filterProfanity` (boolean) — Mask profanity in the transcript. Deepgram and Google only.\n- `cache` (boolean) — Set false (or `\"fresh\"`) to force a new transcription. On by default: an identical recording with identical settings is answered from an earlier transcription for nothing.\n- `save` (boolean) — Set false to skip writing .vtt and .srt files to storage."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/captions"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Transcription queued\",\n  \"data\": {\n    \"id\": \"trs_31c8\",\n    \"status\": \"queued\",\n    \"provider\": \"deepgram\",\n    \"languageCode\": \"en-IN\",\n    \"cached\": false\n  }\n}"
            }
          ]
        },
        {
          "name": "Generate captions (older path)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"audioUrl\": \"https://cdn.acme.com/voice.mp3\",\n  \"languageCode\": \"en-IN\",\n  \"wordsPerCue\": 7\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/transcriptions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transcriptions"
              ]
            },
            "description": "Identical to `POST /v1/captions` — same body, same defaults, same response. Kept because integrations already post here; new work should use `/v1/captions`, which is what the endpoint actually does now that it captions video as well as audio.\n\nRequired scope: `assets:write`\nCost: No render credits. The provider bills your own account per audio-minute — and not at all on a cache hit.\n\nBody parameters:\n- `audioUrl` (string) — URL of the recording. Audio or video. One of this, `assetId` or `renderId` is required.\n- `assetId` (string) — An audio or video asset in your library, instead of a URL.\n- `renderId` (string) — A finished render, instead of a URL.\n- `provider` (string) — Which speech provider. Omit it and the best connected one is chosen.\n- `languageCode` (string) — BCP-47 tag, or `auto` to detect.\n- `wordsPerCue` (number) — Words per caption.\n- `cache` (boolean) — Set false to force a new transcription.\n- `save` (boolean) — Set false to skip writing .vtt and .srt files to storage."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/transcriptions"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Transcription queued\",\n  \"data\": {\n    \"id\": \"trs_31c8\",\n    \"status\": \"queued\",\n    \"provider\": \"deepgram\",\n    \"languageCode\": \"en-IN\",\n    \"cached\": false\n  }\n}"
            }
          ]
        },
        {
          "name": "List transcriptions",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/transcriptions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transcriptions"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Rows to return, 1–100. Defaults to 20.",
                  "disabled": true
                }
              ]
            },
            "description": "Your workspace’s transcriptions, newest first.\n\nRequired scope: `assets:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/transcriptions"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"trs_31c8\",\n      \"status\": \"done\",\n      \"languageCode\": \"en-IN\",\n      \"srtUrl\": \"https://cdn.pixbix.app/media/org_4c1d/caption/trs_31c8.srt\"\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Get a transcription",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/transcriptions/{{id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transcriptions",
                "{{id}}"
              ]
            },
            "description": "One transcription, for polling. Carries the cues, the plain text and the caption file URLs once it is done.\n\nRequired scope: `assets:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/transcriptions/{{id}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"trs_31c8\",\n    \"status\": \"done\",\n    \"text\": \"Diwali offers are live now.\",\n    \"cues\": [\n      {\n        \"start\": 0,\n        \"end\": 2.4,\n        \"text\": \"Diwali offers are live now.\"\n      }\n    ],\n    \"vttUrl\": \"https://cdn.pixbix.app/media/org_4c1d/caption/trs_31c8.vtt\",\n    \"srtUrl\": \"https://cdn.pixbix.app/media/org_4c1d/caption/trs_31c8.srt\"\n  }\n}"
            }
          ]
        },
        {
          "name": "List assets",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/assets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "assets"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "Page number, from 1.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "24",
                  "description": "Rows per page, 1–100. Defaults to 24.",
                  "disabled": true
                },
                {
                  "key": "sort",
                  "value": "createdAt",
                  "description": "Column to order by: createdAt, name, sizeBytes, type, durationSec. Defaults to `createdAt`.",
                  "disabled": true
                },
                {
                  "key": "order",
                  "value": "desc",
                  "description": "Sort direction. Defaults to `desc` for dates and counts.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "diwali",
                  "description": "Case-insensitive match across name, description, tags. Up to 128 characters.",
                  "disabled": true
                },
                {
                  "key": "scope",
                  "value": "workspace",
                  "description": "`workspace` (default) for your own uploads, `global` for the pixbix stock library, `all` for both. The stock library is read-only.",
                  "disabled": true
                },
                {
                  "key": "type",
                  "value": "image",
                  "description": "Filter by media type: `image`, `video`, `audio`, `font`, `lottie` or `svg`. Comma-separate several to match any of them.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "ready",
                  "description": "`ready`, `processing`, `uploading` or `failed`. Only `ready` assets can be rendered with.",
                  "disabled": true
                },
                {
                  "key": "folder",
                  "value": "Backgrounds",
                  "description": "Only assets filed in this collection.",
                  "disabled": true
                },
                {
                  "key": "tags",
                  "value": "harbour,dusk",
                  "description": "One or more tags, comma-separated.",
                  "disabled": true
                }
              ]
            },
            "description": "Media in your workspace library, most recently uploaded first. Set `scope=global` to browse the pixbix stock library instead — the same media the editors show under “Other assets” — or `scope=all` for both at once.\n\nRequired scope: `assets:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/assets"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"ast_19fa\",\n      \"type\": \"image\",\n      \"scope\": \"workspace\",\n      \"name\": \"lamp.jpg\",\n      \"description\": null,\n      \"url\": \"https://cdn.pixbix.app/assets/ast_19fa.jpg\",\n      \"mimeType\": \"image/jpeg\",\n      \"sizeBytes\": 184320,\n      \"width\": 1200,\n      \"height\": 1200,\n      \"durationSec\": null,\n      \"tags\": [\n        \"product\",\n        \"lighting\"\n      ]\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Upload an asset",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/assets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "assets"
              ]
            },
            "description": "multipart/form-data with a single `file` part. Dimensions and duration are probed on upload. Re-uploading identical bytes returns the existing asset rather than storing it twice.\n\nRequired scope: `assets:write`\nCost: Counts against your plan’s storage, not against credits.\n\nBody parameters:\n- `file` (file) — required — The asset itself. Max 200 MB by default.\n- `tags` (string) — Comma-separated keywords, up to 20. Searchable.\n- `description` (string) — Free text, up to 2000 characters. Searchable.\n- `folder` (string) — Collection to file the asset under."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/assets"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"ast_19fa\",\n    \"url\": \"https://cdn.pixbix.app/media/org_4c1d/image/ast_19fa.jpg\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Delete an asset",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/assets/{{assetId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "assets",
                "{{assetId}}"
              ]
            },
            "description": "Removes the asset. Templates already referencing it keep rendering from the cached copy.\n\nRequired scope: `assets:write`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/assets/{{assetId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Asset deleted\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Integrations",
      "description": "Read and write the third-party accounts a workspace has connected — Google Sheets and Drive.",
      "item": [
        {
          "name": "List connected accounts",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations"
              ]
            },
            "description": "Every third-party account this workspace has connected, with the status of each grant. Poll this to notice a connection that has expired before a nightly job discovers it the hard way.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"int_7f21\",\n      \"provider\": \"google-workspace\",\n      \"status\": \"connected\",\n      \"account\": {\n        \"email\": \"ops@acme.in\",\n        \"name\": \"Acme Ops\"\n      },\n      \"connectedAt\": \"2026-08-01T09:12:44.000Z\"\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "List spreadsheets",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/sheets/spreadsheets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "sheets",
                "spreadsheets"
              ],
              "query": [
                {
                  "key": "search",
                  "value": "products",
                  "description": "Filter by name, substring match.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "25",
                  "description": "1–100. Defaults to 25.",
                  "disabled": true
                },
                {
                  "key": "pageToken",
                  "value": "",
                  "description": "Cursor from a previous response’s `nextPageToken`.",
                  "disabled": true
                }
              ]
            },
            "description": "Spreadsheets visible to the connected Google account, most recently modified first. Use it to resolve a name to the id every other Sheets call needs.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/sheets/spreadsheets"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"files\": [\n      {\n        \"id\": \"1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms\",\n        \"name\": \"Product catalogue\",\n        \"url\": \"https://docs.google.com/spreadsheets/d/1BxiMVs0.../edit\",\n        \"modifiedAt\": \"2026-08-12T04:31:00.000Z\"\n      }\n    ],\n    \"nextPageToken\": null\n  }\n}"
            }
          ]
        },
        {
          "name": "Get spreadsheet tabs",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/sheets/spreadsheets/{{spreadsheetId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "sheets",
                "spreadsheets",
                "{{spreadsheetId}}"
              ]
            },
            "description": "A spreadsheet’s tabs and their dimensions. Metadata only — no cell values — so it is cheap enough to call before deciding on a range.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/sheets/spreadsheets/{{spreadsheetId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms\",\n    \"name\": \"Product catalogue\",\n    \"sheets\": [\n      {\n        \"id\": 0,\n        \"title\": \"Products\",\n        \"index\": 0,\n        \"rowCount\": 1000,\n        \"columnCount\": 12\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Read rows",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/sheets/spreadsheets/{{spreadsheetId}}/values",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "sheets",
                "spreadsheets",
                "{{spreadsheetId}}",
                "values"
              ],
              "query": [
                {
                  "key": "range",
                  "value": "Products!A1:F",
                  "description": "A1 notation. A bare tab name reads that whole tab. Defaults to `Sheet1`.",
                  "disabled": true
                },
                {
                  "key": "as",
                  "value": "records",
                  "description": "`records` keys each row by the header row; `values` returns the raw grid.",
                  "disabled": true
                },
                {
                  "key": "renderOption",
                  "value": "FORMATTED_VALUE",
                  "description": "How cells are rendered. Defaults to the formatted value, as displayed in the sheet.",
                  "disabled": true
                }
              ]
            },
            "description": "Read a range. By default the first row is treated as a header and each row comes back as an object keyed by it, which is the shape a render loop wants; pass `as=values` for the raw grid instead.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/sheets/spreadsheets/{{spreadsheetId}}/values"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"range\": \"Products!A1:F51\",\n    \"headers\": [\n      \"sku\",\n      \"name\",\n      \"price\",\n      \"image\"\n    ],\n    \"records\": [\n      {\n        \"sku\": \"AC-100\",\n        \"name\": \"Copper bottle\",\n        \"price\": \"₹899\",\n        \"image\": \"https://cdn.acme.in/ac-100.jpg\"\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Write rows",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"range\": \"Renders!A1\",\n  \"values\": [\n    [\n      \"AC-100\",\n      \"https://cdn.pixbix.app/renders/ac-100.png\",\n      \"2026-08-15\"\n    ],\n    [\n      \"AC-101\",\n      \"https://cdn.pixbix.app/renders/ac-101.png\",\n      \"2026-08-15\"\n    ]\n  ],\n  \"mode\": \"append\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/sheets/spreadsheets/{{spreadsheetId}}/values",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "sheets",
                "spreadsheets",
                "{{spreadsheetId}}",
                "values"
              ]
            },
            "description": "Append rows after the last used row, or overwrite a range outright. Appending is the default because it cannot destroy a cell that was already there — set `mode: \"overwrite\"` deliberately.\n\nRequired scope: `integrations:write`\n\nBody parameters:\n- `range` (string) — required — A1 notation, e.g. `Products!A1`.\n- `values` (array[array]) — required — Rows, each an array of cell values.\n- `mode` (\"append\" | \"overwrite\") — Defaults to `append`, which inserts new rows rather than writing over what follows the range.\n- `raw` (boolean) — Store values verbatim instead of interpreting them as a person typing would — set it for codes that look like dates."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/sheets/spreadsheets/{{spreadsheetId}}/values"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"6 cells written\",\n  \"data\": {\n    \"range\": \"Renders!A12:C13\",\n    \"updatedRows\": 2,\n    \"updatedColumns\": 3,\n    \"updatedCells\": 6,\n    \"mode\": \"append\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Create a spreadsheet",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"title\": \"August campaign renders\",\n  \"sheetTitle\": \"Renders\",\n  \"headers\": [\n    \"sku\",\n    \"render url\",\n    \"created\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/sheets/spreadsheets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "sheets",
                "spreadsheets"
              ]
            },
            "description": "Create a spreadsheet in the connected account, optionally seeded with a header row — useful as the destination for a batch of render URLs.\n\nRequired scope: `integrations:write`\n\nBody parameters:\n- `title` (string) — required — Name of the new spreadsheet.\n- `sheetTitle` (string) — Name of its first tab. Defaults to `Sheet1`.\n- `headers` (array[string]) — Written into the first row."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/sheets/spreadsheets"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Spreadsheet created\",\n  \"data\": {\n    \"id\": \"1n8Kd0PbxA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms\",\n    \"name\": \"August campaign renders\",\n    \"url\": \"https://docs.google.com/spreadsheets/d/1n8Kd0Pbx.../edit\"\n  }\n}"
            }
          ]
        },
        {
          "name": "List Drive files",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/drive/files",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "drive",
                "files"
              ],
              "query": [
                {
                  "key": "folderId",
                  "value": "",
                  "description": "List inside one folder. Omit for the top level.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "logo",
                  "description": "Filter by name, substring match.",
                  "disabled": true
                },
                {
                  "key": "kind",
                  "value": "image",
                  "description": "`image`, `video`, `audio`, `font`, `folder`, or a full MIME type.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "1–200. Defaults to 50.",
                  "disabled": true
                },
                {
                  "key": "pageToken",
                  "value": "",
                  "description": "Cursor from a previous response’s `nextPageToken`.",
                  "disabled": true
                }
              ]
            },
            "description": "Files and folders visible to the connected account, folders first. Visibility is limited to files pixbix created or that you explicitly opened with it — not your whole Drive.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/drive/files"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"files\": [\n      {\n        \"id\": \"1a2B3c4D5e6F\",\n        \"name\": \"brand-logo.png\",\n        \"mimeType\": \"image/png\",\n        \"sizeBytes\": 48211,\n        \"isFolder\": false,\n        \"webViewUrl\": \"https://drive.google.com/file/d/1a2B3c4D5e6F/view\"\n      }\n    ],\n    \"nextPageToken\": null\n  }\n}"
            }
          ]
        },
        {
          "name": "Upload to Drive",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://cdn.pixbix.app/renders/ac-100.png\",\n  \"name\": \"AC-100 poster.png\",\n  \"folderId\": \"1QfolderIdFromDrive\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/drive/files",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "drive",
                "files"
              ]
            },
            "description": "Upload any public URL into a Drive folder — normally a finished render’s `url`. The bytes travel from our storage to Google directly, so the file never passes through your machine.\n\nRequired scope: `integrations:write`\n\nBody parameters:\n- `url` (string) — required — Public http(s) URL to upload.\n- `name` (string) — Filename in Drive. Defaults to the last path segment of the URL.\n- `folderId` (string) — Destination folder. Omit for the account’s root.\n- `mimeType` (string) — Overrides the content type reported by the source URL."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/drive/files"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Uploaded to Drive as “AC-100 poster.png”\",\n  \"data\": {\n    \"id\": \"1a2B3c4D5e6F\",\n    \"name\": \"AC-100 poster.png\",\n    \"mimeType\": \"image/png\",\n    \"sizeBytes\": 48211\n  }\n}"
            }
          ]
        },
        {
          "name": "Create a Drive folder",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"August campaign\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/drive/folders",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "drive",
                "folders"
              ]
            },
            "description": "Create a folder to deliver output into — one per campaign or per client, created as part of the job rather than by hand.\n\nRequired scope: `integrations:write`\n\nBody parameters:\n- `name` (string) — required — Folder name.\n- `parentId` (string) — Parent folder. Omit for the account’s root."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/drive/folders"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Folder created\",\n  \"data\": {\n    \"id\": \"1QfolderIdFromDrive\",\n    \"name\": \"August campaign\",\n    \"isFolder\": true\n  }\n}"
            }
          ]
        },
        {
          "name": "Read Google Contacts",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/contacts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "contacts"
              ],
              "query": [
                {
                  "key": "search",
                  "value": "priya",
                  "description": "Match against name, email or phone. Omit to page through everyone.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "Contacts per page, up to 1000.",
                  "disabled": true
                },
                {
                  "key": "pageToken",
                  "value": "",
                  "description": "From the previous response, to fetch the next page.",
                  "disabled": true
                }
              ]
            },
            "description": "The connected account's address book, one flat object per person. The People API returns every field as an array of objects with metadata about which entry is primary; this lifts the primary value of each to a top-level key, so a contact drops straight into a template as {{item.firstName}} without any reshaping. Read-only — pixbix never writes to your contacts. Pass `search` to look someone up, or page with `pageToken` to walk the whole book.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/contacts"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Contacts\",\n  \"data\": {\n    \"contacts\": [\n      {\n        \"id\": \"people/c1234567890\",\n        \"displayName\": \"Priya Sharma\",\n        \"firstName\": \"Priya\",\n        \"lastName\": \"Sharma\",\n        \"email\": \"priya@acme.in\",\n        \"allEmails\": \"priya@acme.in, priya.sharma@gmail.com\",\n        \"phone\": \"+91 98765 43210\",\n        \"allPhones\": \"+91 98765 43210\",\n        \"organization\": \"Acme Retail\",\n        \"jobTitle\": \"Head of Marketing\",\n        \"city\": \"Mumbai\",\n        \"country\": \"India\",\n        \"photoUrl\": \"https://lh3.googleusercontent.com/…\",\n        \"notes\": \"\",\n        \"groupIds\": [\n          \"contactGroups/myContacts\"\n        ]\n      }\n    ],\n    \"nextPageToken\": \"CJHm2wIQAQ\",\n    \"total\": 412\n  }\n}"
            }
          ]
        },
        {
          "name": "List contact labels",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/contacts/groups",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "contacts",
                "groups"
              ]
            },
            "description": "The labels the connected account keeps in Google Contacts, largest first, each with its member count. These are what a workflow points at to narrow a run to \"Clients\" rather than everyone. Empty labels are omitted, since targeting one would produce a run with nothing in it.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/contacts/groups"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"3 label(s)\",\n  \"data\": [\n    {\n      \"id\": \"contactGroups/myContacts\",\n      \"name\": \"My Contacts\",\n      \"count\": 412\n    },\n    {\n      \"id\": \"contactGroups/7a3f1c2e\",\n      \"name\": \"Clients\",\n      \"count\": 86\n    },\n    {\n      \"id\": \"contactGroups/starred\",\n      \"name\": \"Starred\",\n      \"count\": 12\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Import a Drive file",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"folder\": \"brand\",\n  \"tags\": \"logo,brand\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-workspace/drive/files/{{fileId}}/import",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-workspace",
                "drive",
                "files",
                "{{fileId}}",
                "import"
              ]
            },
            "description": "Copy a Drive file into your pixbix media library, where templates can reference it. Goes through the same path as an upload, so it deduplicates by content and counts against your storage allowance. Google-native documents (Docs, Sheets, Slides) are refused — export them first.\n\nRequired scope: `assets:write`\n\nBody parameters:\n- `folder` (string) — Media-library folder to file it under.\n- `tags` (string) — Comma-separated tags."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-workspace/drive/files/{{fileId}}/import"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Uploaded\",\n  \"data\": {\n    \"id\": \"ast_19fa\",\n    \"name\": \"brand-logo.png\",\n    \"type\": \"image\",\n    \"sizeBytes\": 48211\n  }\n}"
            }
          ]
        },
        {
          "name": "List voices",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/elevenlabs/voices",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "elevenlabs",
                "voices"
              ]
            },
            "description": "Voices available to the connected ElevenLabs account, including your own clones. Use a voice id with the speech endpoint.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/elevenlabs/voices"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"21m00Tcm4TlvDq8ikWAM\",\n      \"name\": \"Rachel\",\n      \"category\": \"premade\",\n      \"previewUrl\": \"https://storage.googleapis.com/eleven-public-prod/…/sample.mp3\",\n      \"labels\": {\n        \"accent\": \"american\",\n        \"gender\": \"female\"\n      }\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "List speech models",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/elevenlabs/models",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "elevenlabs",
                "models"
              ]
            },
            "description": "Speech models the connected plan can use, with the languages each supports and its per-request character ceiling. Voice-changer models are filtered out — only models that synthesise from text are returned.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/elevenlabs/models"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"eleven_multilingual_v2\",\n      \"name\": \"Eleven Multilingual v2\",\n      \"languages\": [\n        {\n          \"id\": \"en\",\n          \"name\": \"English\"\n        },\n        {\n          \"id\": \"hi\",\n          \"name\": \"Hindi\"\n        }\n      ],\n      \"maxCharacters\": 5000\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Check character allowance",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/elevenlabs/usage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "elevenlabs",
                "usage"
              ]
            },
            "description": "Characters used and remaining on the connected ElevenLabs plan, and when the allowance resets. Check this before a long batch — synthesis is billed by ElevenLabs, not by pixbix.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/elevenlabs/usage"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"tier\": \"creator\",\n    \"characterCount\": 41200,\n    \"characterLimit\": 100000,\n    \"charactersRemaining\": 58800,\n    \"resetsAt\": \"2026-09-01T00:00:00.000Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Generate a voice-over",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"text\": \"Diwali sale — fifty percent off, this week only.\",\n  \"voiceId\": \"21m00Tcm4TlvDq8ikWAM\",\n  \"modelId\": \"eleven_multilingual_v2\",\n  \"name\": \"diwali-vo.mp3\",\n  \"folder\": \"voice-overs\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/elevenlabs/speech",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "elevenlabs",
                "speech"
              ]
            },
            "description": "Synthesises a script into an MP3 and saves it to your media library, returning the asset. Drop the resulting URL onto a timeline as an audio clip — and pass the same script as a caption `script` with the clip as its `audio`, and the engine times the captions against the voice for you. Costs characters on your ElevenLabs plan; pixbix charges no render credits.\n\nRequired scope: `assets:write`\nCost: No pixbix credits. Consumes characters from your own ElevenLabs plan, and storage from your pixbix quota.\n\nBody parameters:\n- `text` (string) — required — The script to speak. Up to 5,000 characters per request.\n- `voiceId` (string) — required — From the voices endpoint.\n- `modelId` (string) — Defaults to eleven_multilingual_v2.\n- `stability` (number) — 0–1. Low is expressive and varies between takes; high is consistent and flatter.\n- `similarityBoost` (number) — 0–1. How closely the output holds to the original voice.\n- `style` (number) — 0–1. Delivery emphasis. Adds latency above 0.\n- `name` (string) — Filename in the media library. Defaults to the opening words of the script.\n- `folder` (string) — Media-library folder to file it under."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/elevenlabs/speech"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Uploaded\",\n  \"data\": {\n    \"id\": \"ast_4b7c\",\n    \"name\": \"diwali-vo.mp3\",\n    \"type\": \"audio\",\n    \"mimeType\": \"audio/mpeg\",\n    \"sizeBytes\": 24118,\n    \"url\": \"https://cdn.pixbix.app/media/org_4c1d/audio/4b7c.mp3\"\n  }\n}"
            }
          ]
        },
        {
          "name": "List caption languages",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-speech/languages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-speech",
                "languages"
              ]
            },
            "description": "The languages the caption pickers offer, as BCP-47 codes. A curated list rather than Google’s full set — any code Google accepts still works when passed to the captions endpoint.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-speech/languages"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"code\": \"en-IN\",\n      \"label\": \"English (India)\"\n    },\n    {\n      \"code\": \"hi-IN\",\n      \"label\": \"Hindi\"\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Generate captions",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"assetId\": \"ast_4b7c\",\n  \"languageCode\": \"en-IN\",\n  \"wordsPerCue\": 4\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/google-speech/captions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "google-speech",
                "captions"
              ]
            },
            "description": "Transcribes a voice-over into captions timed to the word. Returns cues shaped exactly like the engine’s `caption` asset — drop them straight onto a timeline for burned-in karaoke captions — plus finished WebVTT and SRT text, saved to your media library unless you pass `save: false`. Name the audio however you have it: `audioUrl`, an `assetId` from your library, or a finished `renderId`.\n\nRequired scope: `assets:write`\nCost: No pixbix credits. Google bills your own Cloud project per audio-minute, and the two subtitle files count against your storage quota.\n\nBody parameters:\n- `audioUrl` (string) — A public MP3, WAV, FLAC or OGG. A video file is refused — transcribe the voice-over that went into it.\n- `assetId` (string) — An audio asset in your media library, instead of a URL.\n- `renderId` (string) — A finished audio render, instead of a URL.\n- `languageCode` (string) — BCP-47. Defaults to en-US. Getting this wrong returns confident nonsense rather than an error.\n- `alternativeLanguageCodes` (array) — Up to three more languages Google may pick from — worth setting for audio that switches mid-sentence.\n- `wordsPerCue` (number) — 1–12, default 4. Captions also break at a full stop and at any pause over a second.\n- `punctuation` (boolean) — Infer full stops and commas. On by default; captions read badly without them.\n- `filterProfanity` (boolean) — Mask strong language. Off by default — Google’s filter is heavy-handed.\n- `save` (boolean) — Write the .vtt and .srt to your media library. On by default; pass false for the cues alone.\n- `name` (string) — Base filename for the saved files. Defaults to the audio’s own name."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/google-speech/captions"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"9 captions from 12.4s of audio\",\n  \"data\": {\n    \"cues\": [\n      {\n        \"start\": 0.2,\n        \"end\": 1.9,\n        \"text\": \"Hi Priya, your order\",\n        \"words\": [\n          {\n            \"start\": 0.2,\n            \"end\": 0.5,\n            \"text\": \"Hi\"\n          }\n        ]\n      }\n    ],\n    \"text\": \"Hi Priya, your order is on its way.\",\n    \"languageCode\": \"en-IN\",\n    \"durationSec\": 12.4,\n    \"confidence\": 0.9421,\n    \"wordCount\": 34,\n    \"vtt\": \"WEBVTT\\n\\n1\\n00:00:00.200 --> 00:00:01.900\\nHi Priya, your order\\n\",\n    \"srt\": \"1\\n00:00:00,200 --> 00:00:01,900\\nHi Priya, your order\\n\",\n    \"files\": {\n      \"vttUrl\": \"https://cdn.pixbix.app/media/org_4c1d/captions/lz4k-diwali-vo.vtt\",\n      \"srtUrl\": \"https://cdn.pixbix.app/media/org_4c1d/captions/lz4k-diwali-vo.srt\"\n    }\n  }\n}"
            }
          ]
        },
        {
          "name": "List messaging channels",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/motherbot/channels",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "motherbot",
                "channels"
              ],
              "query": [
                {
                  "key": "channel",
                  "value": "whatsapp",
                  "description": "Restrict to one channel: whatsapp, sms, rcs, email, telegram, line, viber, messenger, instagram or webchat.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "connected",
                  "description": "Defaults to connected.",
                  "disabled": true
                }
              ]
            },
            "description": "Every sending identity the connected MotherBot workspace has — WhatsApp numbers, SMS and email senders, and the chat channels — each with what it can actually carry. Read this first: `capabilities` is derived from the account itself, so it is the honest answer to “can I send buttons on this”, and `id` is what to pass as `accountId` when sending.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/motherbot/channels"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"66f1c2a0d3b4e5f6a7b8c9d0\",\n      \"channel\": \"whatsapp\",\n      \"provider\": \"meta\",\n      \"label\": \"Acme Retail\",\n      \"identifier\": \"919876543210\",\n      \"isDefault\": true,\n      \"status\": \"connected\",\n      \"capabilities\": {\n        \"template\": true,\n        \"text\": true,\n        \"media\": [\n          \"image\",\n          \"video\",\n          \"document\",\n          \"audio\"\n        ],\n        \"interactive\": [\n          \"button\",\n          \"list\",\n          \"cta_url\"\n        ]\n      }\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "List WhatsApp templates",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/motherbot/templates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "motherbot",
                "templates"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "APPROVED",
                  "description": "APPROVED (default), PENDING, REJECTED, DISABLED or DRAFT.",
                  "disabled": true
                },
                {
                  "key": "category",
                  "value": "MARKETING",
                  "description": "MARKETING, UTILITY or AUTHENTICATION.",
                  "disabled": true
                },
                {
                  "key": "language",
                  "value": "en",
                  "description": "Language code, e.g. en_US or hi.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "order",
                  "description": "Partial name match.",
                  "disabled": true
                },
                {
                  "key": "accountId",
                  "value": "66f1c2a0d3b4e5f6a7b8c9d0",
                  "description": "Only templates approved on this WhatsApp number. Templates are approved per number, so a send from one number cannot use another’s.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "100",
                  "description": "Up to 100 per page.",
                  "disabled": true
                }
              ]
            },
            "description": "The WhatsApp templates approved on the connected MotherBot account. `headerFormat` is the field that matters: only a template whose header is `VIDEO` can deliver a rendered video, and `bodyVariableCount` is how many `{{n}}` slots you must fill. Approved templates only unless you pass `status`.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/motherbot/templates"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"66f1c2a0d3b4e5f6a7b8c9d0\",\n      \"accountId\": \"66f1c2a0d3b4e5f6a7b8c9d0\",\n      \"name\": \"order_ready_video\",\n      \"category\": \"MARKETING\",\n      \"language\": \"en\",\n      \"status\": \"APPROVED\",\n      \"headerFormat\": \"VIDEO\",\n      \"bodyVariableCount\": 2,\n      \"variables\": [\n        \"name\",\n        \"order_id\"\n      ],\n      \"usageCount\": 1284\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "List WhatsApp contacts",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/motherbot/contacts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "motherbot",
                "contacts"
              ],
              "query": [
                {
                  "key": "tag",
                  "value": "leads",
                  "description": "Only contacts carrying this tag.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "priya",
                  "description": "Partial name or number match.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "100",
                  "description": "Up to 100 per page.",
                  "disabled": true
                },
                {
                  "key": "page",
                  "value": "1",
                  "description": "1-based page number.",
                  "disabled": true
                }
              ]
            },
            "description": "The contact list on the connected MotherBot account, filterable by tag. `waId` is the number in WhatsApp’s own form, which is what the send endpoint wants. Contacts who opted out or are blocked are returned with those flags set — do not message them.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/motherbot/contacts"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"contacts\": [\n      {\n        \"id\": \"66f1c2a0d3b4e5f6a7b8c9d1\",\n        \"waId\": \"919876543210\",\n        \"name\": \"Priya Sharma\",\n        \"firstName\": \"Priya\",\n        \"lastName\": \"Sharma\",\n        \"email\": \"priya@acme.in\",\n        \"tags\": [\n          \"leads\",\n          \"diwali-2026\"\n        ],\n        \"optedIn\": true,\n        \"blocked\": false\n      }\n    ],\n    \"total\": 412,\n    \"page\": 1,\n    \"pages\": 5\n  }\n}"
            }
          ]
        },
        {
          "name": "Check WhatsApp allowance",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/motherbot/usage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "motherbot",
                "usage"
              ]
            },
            "description": "The plan, the per-minute rate limit for your key and every quota on the connected MotherBot account, including messages left this month. Read this before a batch — sends are billed by MotherBot, not by pixbix, and a run that discovers the ceiling halfway through has already spent the renders.\n\nRequired scope: `integrations:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/motherbot/usage"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"plan\": \"growth\",\n    \"subscriptionStatus\": \"active\",\n    \"active\": true,\n    \"rateLimitPerMinute\": 300,\n    \"messages\": {\n      \"key\": \"messagesPerMonth\",\n      \"label\": \"Messages\",\n      \"used\": 8420,\n      \"limit\": 50000,\n      \"unlimited\": false,\n      \"remaining\": 41580,\n      \"percentUsed\": 17\n    }\n  }\n}"
            }
          ]
        },
        {
          "name": "Send a render on WhatsApp",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"to\": \"919876543210\",\n  \"templateName\": \"order_ready_video\",\n  \"renderId\": \"rnd_8fK2mQ\",\n  \"variables\": {\n    \"1\": \"Priya\",\n    \"2\": \"AC-4192\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/motherbot/messages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "motherbot",
                "messages"
              ]
            },
            "description": "Sends one approved template to one number from your WhatsApp Business number, with a render in the template’s media header. Pass `renderId` and the finished file’s URL is looked up for you — no polling, no second call. WhatsApp only lets a business open a conversation with an approved template; `type: \"text\"` is accepted and reaches only contacts who messaged you in the last 24 hours.\n\nRequired scope: `integrations:write`\nCost: No pixbix credits. Consumes a conversation on your own MotherBot plan.\n\nBody parameters:\n- `to` (string) — required — How the chosen channel addresses somebody: a phone number for WhatsApp, SMS and RCS (international form, digits only — a leading zero, spaces and a “+” are cleaned up, and a number with no country code is refused rather than guessed at), an email address for email, or the platform id for a chat channel.\n- `channel` (string) — Defaults to whatsapp — the only channel that can open a conversation. A type the channel cannot carry is refused rather than silently downgraded.\n- `accountId` (string) — Which connected identity to send from, from the channels endpoint. Defaults to the workspace’s default for the channel.\n- `type` (string) — template (default), text, image, video, document, audio or interactive.\n- `templateName` (string) — required — An approved template. Required for template sends.\n- `languageCode` (string) — Only needed when one template name is approved in several languages.\n- `renderId` (string) — A finished render in this workspace; its URL goes in the template’s media header.\n- `mediaUrl` (string) — Any public URL, if you would rather name the file directly — a render, a capture, or a file you host. Takes precedence over renderId. Its type must match what the template was approved for; a mismatch is refused here rather than per recipient by Meta.\n- `variables` (object) — Body variables by slot number: { \"1\": \"Priya\", \"2\": \"AC-4192\" }. Numbered, because WhatsApp fills them positionally.\n- `text` (string) — The message body, for type: \"text\".\n- `subject` (string) — Required on the email channel; ignored elsewhere.\n- `caption` (string) — Shown under an image, video or document sent on its own. Audio carries no caption.\n- `interactive` (object) — For type: \"interactive\". Meta’s own shape: { type: \"button\" | \"list\" | \"cta_url\", body: { text }, header?, footer?, action }. Reply buttons cap at 3, list rows at 10 overall, and both are checked here rather than at Meta.\n- `headerFilename` (string) — Filename shown for a document header. Derived from the URL when omitted, and ignored for image and video."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/motherbot/messages"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Sent to 919876543210\",\n  \"data\": {\n    \"messageId\": \"wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSMzk2…\",\n    \"to\": \"919876543210\",\n    \"status\": \"sent\",\n    \"contactId\": \"66f1c2a0d3b4e5f6a7b8c9d1\",\n    \"contactCreated\": false\n  }\n}"
            }
          ]
        },
        {
          "name": "Broadcast a render on WhatsApp",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Diwali 2026 — video\",\n  \"templateName\": \"diwali_offer_video\",\n  \"renderId\": \"rnd_8fK2mQ\",\n  \"commonVariables\": {\n    \"2\": \"50%\"\n  },\n  \"recipients\": [\n    {\n      \"to\": \"919876543210\",\n      \"name\": \"Priya\",\n      \"variables\": {\n        \"1\": \"Priya\"\n      }\n    },\n    {\n      \"to\": \"919812345678\",\n      \"name\": \"Arjun\",\n      \"variables\": {\n        \"1\": \"Arjun\"\n      }\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/integrations/motherbot/campaigns",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "integrations",
                "motherbot",
                "campaigns"
              ]
            },
            "description": "Queues one broadcast of the SAME render to many numbers, paced by MotherBot — which protects the number’s quality rating in a way a loop of individual sends does not. For personalised video, where each recipient gets a different file, call the send endpoint once per person instead: a campaign carries a single media URL for the whole run.\n\nRequired scope: `integrations:write`\nCost: No pixbix credits. Consumes one conversation per recipient on your own MotherBot plan.\n\nBody parameters:\n- `templateName` (string) — required — An approved template.\n- `recipients` (array) — required — Up to 5,000 per campaign. Each entry takes `to`, an optional `name`, and optional per-recipient `variables` that override the common ones.\n- `name` (string) — What the campaign is called in MotherBot. Defaults to “pixbix — <today>”.\n- `languageCode` (string) — Only needed when one template name is approved in several languages.\n- `commonVariables` (object) — Body variables shared by every recipient, by slot number: { \"2\": \"50%\" }.\n- `renderId` (string) — A finished render in this workspace, used as the campaign’s media header.\n- `mediaUrl` (string) — A public URL instead of a render id.\n- `rateLimitPerSecond` (number) — Messages a second. MotherBot’s own default is 3."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/integrations/motherbot/campaigns"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Campaign queued for 2 recipient(s) — ~1 second\",\n  \"data\": {\n    \"campaignId\": \"66f1c2a0d3b4e5f6a7b8c9d2\",\n    \"status\": \"running\",\n    \"total\": 2,\n    \"rateLimitPerSecond\": 3,\n    \"etaSeconds\": 1,\n    \"etaText\": \"~1 second\"\n  }\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Webhooks",
      "description": "Register and configure the endpoints we deliver render events to.",
      "item": [
        {
          "name": "List webhook endpoints",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "Page number, from 1.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "Rows per page, 1–100. Defaults to 50.",
                  "disabled": true
                },
                {
                  "key": "sort",
                  "value": "createdAt",
                  "description": "Column to order by: createdAt, description, url, lastSuccessAt, lastFailureAt. Defaults to `createdAt`.",
                  "disabled": true
                },
                {
                  "key": "order",
                  "value": "desc",
                  "description": "Sort direction. Defaults to `desc` for dates and counts.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "diwali",
                  "description": "Case-insensitive match across description, url. Up to 128 characters.",
                  "disabled": true
                },
                {
                  "key": "isActive",
                  "value": "true",
                  "description": "Restrict to live or paused endpoints.",
                  "disabled": true
                },
                {
                  "key": "events",
                  "value": "render.completed",
                  "description": "Only endpoints subscribed to these events, comma-separated.",
                  "disabled": true
                }
              ]
            },
            "description": "Every delivery endpoint registered in the workspace, newest first. The signing secret is never returned — only a hint at its last four characters, so you can tell two endpoints apart without the secret being readable after creation.\n\nRequired scope: `webhooks:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/webhooks"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"whk_3d81\",\n      \"url\": \"https://acme.example/hooks/pixbix\",\n      \"description\": \"Production\",\n      \"events\": [\n        \"render.completed\",\n        \"render.failed\",\n        \"render.cancelled\"\n      ],\n      \"method\": \"POST\",\n      \"bodyFormat\": \"json\",\n      \"payloadMode\": \"default\",\n      \"secretHint\": \"whsec_…8f2a\",\n      \"isActive\": true,\n      \"consecutiveFailures\": 0,\n      \"lastSuccessAt\": \"2026-08-15T09:41:02.118Z\"\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Register a webhook endpoint",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://acme.example/hooks/pixbix\",\n  \"description\": \"Production\",\n  \"events\": [\n    \"render.completed\",\n    \"render.failed\",\n    \"render.cancelled\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ]
            },
            "description": "Registers an endpoint and returns its signing secret. The secret appears in this response and nowhere else — store it before moving on, because it cannot be read again. Counts against the plan's webhook allowance.\n\nRequired scope: `webhooks:write`\n\nBody parameters:\n- `url` (string) — required — Absolute HTTPS URL. May contain {{variables}}, resolved per delivery.\n- `description` (string) — Your own label. Up to 200 characters.\n- `events` (string[]) — Defaults to the three terminal endings: render.completed, render.failed and render.cancelled.\n- `method` (\"POST\" | \"PUT\" | \"PATCH\" | \"GET\") — Defaults to POST.\n- `bodyFormat` (\"json\" | \"form\" | \"multipart\" | \"none\") — How the body is encoded. Defaults to json.\n- `payloadMode` (\"default\" | \"custom\") — Send our event envelope, or your own body built from {{variables}}.\n- `payloadTemplate` (string) — The custom body, when payloadMode is custom. Required in that case.\n- `headers` ({ key, value }[]) — Up to 20 extra request headers. Values accept {{variables}}."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/webhooks"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Webhook created — copy the secret now\",\n  \"data\": {\n    \"id\": \"whk_3d81\",\n    \"url\": \"https://acme.example/hooks/pixbix\",\n    \"events\": [\n      \"render.completed\",\n      \"render.failed\",\n      \"render.cancelled\"\n    ],\n    \"isActive\": true,\n    \"secret\": \"whsec_Q8vN2pR7yTt0mK4dLwXhAe6c\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Update a webhook endpoint",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"events\": [\n    \"render.completed\"\n  ],\n  \"isActive\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhookId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhookId}}"
              ]
            },
            "description": "Changes only the fields you send, so a call that toggles isActive leaves the request configuration alone. Re-enabling a disabled endpoint clears the failure streak that disabled it.\n\nRequired scope: `webhooks:write`\n\nBody parameters:\n- `isActive` (boolean) — Pause or resume deliveries.\n- `url` (string) — Absolute HTTPS URL.\n- `events` (string[]) — Replaces the subscribed event list.\n- `method` (\"POST\" | \"PUT\" | \"PATCH\" | \"GET\") — HTTP method used for delivery.\n- `bodyFormat` (\"json\" | \"form\" | \"multipart\" | \"none\") — Body encoding.\n- `payloadMode` (\"default\" | \"custom\") — Our envelope, or your own body.\n- `payloadTemplate` (string) — The custom body, when payloadMode is custom.\n- `headers` ({ key, value }[]) — Replaces the extra header list."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "PATCH",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/webhooks/{{webhookId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Webhook updated\",\n  \"data\": {\n    \"id\": \"whk_3d81\",\n    \"events\": [\n      \"render.completed\"\n    ],\n    \"isActive\": true,\n    \"consecutiveFailures\": 0\n  }\n}"
            }
          ]
        },
        {
          "name": "Send a test event",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhookId}}/test",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhookId}}",
                "test"
              ]
            },
            "description": "Delivers one sample render.completed event, built and signed exactly like a real one — so it exercises the configured method, encoding, headers and payload rather than a simplified stand-in. Carries X-Pixbix-Test: true, and a failure does not count towards the auto-disable streak.\n\nRequired scope: `webhooks:write`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/webhooks/{{webhookId}}/test"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Delivered — your endpoint replied 200.\",\n  \"data\": {\n    \"ok\": true,\n    \"status\": 200\n  }\n}"
            }
          ]
        },
        {
          "name": "Delete a webhook endpoint",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhookId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhookId}}"
              ]
            },
            "description": "Deliveries stop immediately. Renders are unaffected — results stay readable from GET /v1/render/:id, which is the fallback when a delivery never arrives.\n\nRequired scope: `webhooks:write`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/webhooks/{{webhookId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Webhook deleted\",\n  \"data\": null\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Workflows",
      "description": "Start and inspect saved automations — the unattended path from a spreadsheet to delivered creatives.",
      "item": [
        {
          "name": "List workflows",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/workflows",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "workflows"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "Page number, from 1.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Rows per page, 1–100. Defaults to 20.",
                  "disabled": true
                },
                {
                  "key": "sort",
                  "value": "updatedAt",
                  "description": "Column to order by: updatedAt, createdAt, name, lastRunAt, nextRunAt. Defaults to `updatedAt`.",
                  "disabled": true
                },
                {
                  "key": "order",
                  "value": "desc",
                  "description": "Sort direction. Defaults to `desc` for dates and counts.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "diwali",
                  "description": "Case-insensitive match across name, description. Up to 128 characters.",
                  "disabled": true
                },
                {
                  "key": "isActive",
                  "value": "true",
                  "description": "Restrict to active or paused workflows.",
                  "disabled": true
                },
                {
                  "key": "active",
                  "value": "true",
                  "description": "Older spelling of `isActive`, still accepted.",
                  "disabled": true
                },
                {
                  "key": "trigger",
                  "value": "schedule",
                  "description": "manual, schedule, event, hook or poll. Comma-separate several.",
                  "disabled": true
                }
              ]
            },
            "description": "Every workflow in the workspace, most recently changed first, with its trigger, its next scheduled run and how the last one went.\n\nRequired scope: `workflows:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/workflows"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"wfl_7c1f9a\",\n      \"name\": \"Daily product posters\",\n      \"isActive\": true,\n      \"trigger\": {\n        \"type\": \"schedule\",\n        \"cron\": \"0 6 * * *\",\n        \"timezone\": \"Asia/Kolkata\"\n      },\n      \"scheduleDescription\": \"At 06:00 (Asia/Kolkata)\",\n      \"nextRunAt\": \"2026-08-16T00:30:00.000Z\",\n      \"lastRunStatus\": \"succeeded\",\n      \"stats\": {\n        \"runs\": 42,\n        \"succeeded\": 41,\n        \"failed\": 1,\n        \"itemsProcessed\": 1260\n      }\n    }\n  ],\n  \"pagination\": {\n    \"page\": 1,\n    \"limit\": 20,\n    \"total\": 1,\n    \"totalPages\": 1\n  }\n}"
            }
          ]
        },
        {
          "name": "Get a workflow",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/workflows/{{workflowId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "workflows",
                "{{workflowId}}"
              ]
            },
            "description": "One workflow, including its full step list — useful for checking what a run is about to do before you fire it. The inbound hook token is never returned here; it is readable only in the dashboard.\n\nRequired scope: `workflows:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/workflows/{{workflowId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"wfl_7c1f9a\",\n    \"name\": \"Daily product posters\",\n    \"isActive\": true,\n    \"maxItemsPerRun\": 100,\n    \"skipIfRunning\": true,\n    \"trigger\": {\n      \"type\": \"schedule\",\n      \"cron\": \"0 6 * * *\",\n      \"timezone\": \"Asia/Kolkata\"\n    },\n    \"steps\": [\n      {\n        \"id\": \"stp_a1\",\n        \"type\": \"source.sheet\",\n        \"name\": \"Read a spreadsheet\",\n        \"isEnabled\": true\n      },\n      {\n        \"id\": \"stp_b2\",\n        \"type\": \"filter\",\n        \"name\": \"Keep only some rows\",\n        \"isEnabled\": true\n      },\n      {\n        \"id\": \"stp_c3\",\n        \"type\": \"render.template\",\n        \"name\": \"Render a template\",\n        \"isEnabled\": true\n      },\n      {\n        \"id\": \"stp_d4\",\n        \"type\": \"deliver.sheet\",\n        \"name\": \"Write back to a spreadsheet\",\n        \"isEnabled\": true\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Run a workflow",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"payload\": {\n    \"campaign\": \"diwali-2026\",\n    \"region\": \"west\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/workflows/{{workflowId}}/run",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "workflows",
                "{{workflowId}}",
                "run"
              ]
            },
            "description": "Starts a run and returns immediately with its id — a run over hundreds of rows takes minutes, so holding the request open for it would time out at every proxy in between. Poll the run, or let the workflow's own delivery steps tell you. If a run is already in flight and the workflow skips overlaps, the response says `skipped` and nothing is started.\n\nRequired scope: `workflows:run`\nCost: Free to start. The renders and captures inside the run cost their usual credits, and the run itself counts against your monthly workflow-run allowance.\n\nBody parameters:\n- `payload` (object) — Optional. Seeds the run and is readable in every step as {{trigger.payload.…}} — how a deployment pipeline passes a release name or a campaign id into the creatives."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/workflows/{{workflowId}}/run"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Workflow started\",\n  \"data\": {\n    \"id\": \"wfr_2b8e41\",\n    \"workflowId\": \"wfl_7c1f9a\",\n    \"status\": \"running\",\n    \"trigger\": {\n      \"type\": \"manual\",\n      \"detail\": null\n    },\n    \"startedAt\": \"2026-08-15T09:42:11.204Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "List runs",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/workflows/{{workflowId}}/runs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "workflows",
                "{{workflowId}}",
                "runs"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "Page number, from 1.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Rows per page, 1–100. Defaults to 20.",
                  "disabled": true
                },
                {
                  "key": "sort",
                  "value": "createdAt",
                  "description": "Column to order by: createdAt, startedAt, finishedAt, status. Defaults to `createdAt`.",
                  "disabled": true
                },
                {
                  "key": "order",
                  "value": "desc",
                  "description": "Sort direction. Defaults to `desc` for dates and counts.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "failed",
                  "description": "queued, running, succeeded, partial, failed, cancelled or skipped. Comma-separate several to match any of them.",
                  "disabled": true
                },
                {
                  "key": "trigger",
                  "value": "schedule",
                  "description": "What started the run: manual, schedule, event, hook or poll.",
                  "disabled": true
                },
                {
                  "key": "createdFrom",
                  "value": "2026-08-01",
                  "description": "ISO date. Only runs started on or after it.",
                  "disabled": true
                },
                {
                  "key": "createdTo",
                  "value": "2026-08-31",
                  "description": "ISO date. Only runs started on or before it.",
                  "disabled": true
                }
              ]
            },
            "description": "The run history for one workflow, newest first. The step breakdown and log are omitted here — fetch a single run for those.\n\nRequired scope: `workflows:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/workflows/{{workflowId}}/runs"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"wfr_2b8e41\",\n      \"status\": \"partial\",\n      \"trigger\": {\n        \"type\": \"schedule\",\n        \"detail\": \"0 6 * * *\"\n      },\n      \"itemsIn\": 30,\n      \"itemsSucceeded\": 28,\n      \"itemsFailed\": 2,\n      \"creditsSpent\": 28,\n      \"durationMs\": 84210,\n      \"finishedAt\": \"2026-08-15T00:31:24.118Z\"\n    }\n  ],\n  \"pagination\": {\n    \"page\": 1,\n    \"limit\": 20,\n    \"total\": 1,\n    \"totalPages\": 1\n  }\n}"
            }
          ]
        },
        {
          "name": "Get a run",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{apiKey}}",
                "description": "Workspace API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/workflows/runs/{{runId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "workflows",
                "runs",
                "{{runId}}"
              ]
            },
            "description": "One run in full: the per-step breakdown with item counts on each side, the capped log, the ids of every render it created, and the first few results. This is the endpoint to poll after starting a run, and the one to read when an overnight automation did something unexpected.\n\nRequired scope: `workflows:read`"
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/workflows/runs/{{runId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"wfr_2b8e41\",\n    \"workflowId\": \"wfl_7c1f9a\",\n    \"status\": \"partial\",\n    \"itemsIn\": 30,\n    \"itemsSucceeded\": 28,\n    \"itemsFailed\": 2,\n    \"creditsSpent\": 28,\n    \"steps\": [\n      {\n        \"stepId\": \"stp_a1\",\n        \"type\": \"source.sheet\",\n        \"status\": \"succeeded\",\n        \"itemsIn\": 1,\n        \"itemsOut\": 30,\n        \"message\": \"30 rows from Products!A1:F500\"\n      },\n      {\n        \"stepId\": \"stp_c3\",\n        \"type\": \"render.template\",\n        \"status\": \"succeeded\",\n        \"itemsIn\": 30,\n        \"itemsOut\": 28,\n        \"itemsFailed\": 2,\n        \"message\": \"Rendered 28 of 30, 2 failed\"\n      },\n      {\n        \"stepId\": \"stp_d4\",\n        \"type\": \"deliver.sheet\",\n        \"status\": \"succeeded\",\n        \"itemsIn\": 28,\n        \"itemsOut\": 28,\n        \"message\": \"Wrote 28 rows to Results!A31:B58\"\n      }\n    ],\n    \"renderJobIds\": [\n      \"rnd_8f2a1c\",\n      \"rnd_8f2a1d\"\n    ],\n    \"error\": null,\n    \"durationMs\": 84210\n  }\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Webhooks (workspace)",
      "description": "Register and configure the endpoints we deliver events to. Authenticated with a dashboard session token plus `X-Pixbix-Org`, not with an API key — set `authToken` and `orgId` in the environment first.",
      "item": [
        {
          "name": "List webhook endpoints",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/organization/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "organization",
                "webhooks"
              ]
            },
            "description": "Every endpoint in the workspace, newest first. The signing secret is never returned — only `secretHint`."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/organization/webhooks"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"whe_7c1f9a\",\n      \"url\": \"https://api.acme.com/hooks/pixbix\",\n      \"description\": \"Production render handler\",\n      \"events\": [\n        \"render.completed\",\n        \"render.failed\",\n        \"render.cancelled\"\n      ],\n      \"method\": \"POST\",\n      \"bodyFormat\": \"json\",\n      \"payloadMode\": \"custom\",\n      \"payloadTemplate\": \"{\\n  \\\"event\\\": \\\"{{event.type}}\\\",\\n  \\\"renderId\\\": \\\"{{data.id}}\\\"\\n}\",\n      \"headers\": [\n        {\n          \"key\": \"Authorization\",\n          \"value\": \"Bearer your-receiver-token\"\n        }\n      ],\n      \"secretHint\": \"whsec_…8f2a\",\n      \"isActive\": true,\n      \"consecutiveFailures\": 0,\n      \"lastSuccessAt\": null,\n      \"createdAt\": \"2026-08-15T09:42:11.204Z\"\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Create a webhook endpoint",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://api.acme.com/hooks/pixbix\",\n  \"description\": \"Production render handler\",\n  \"events\": [\n    \"render.completed\",\n    \"render.failed\",\n    \"render.cancelled\"\n  ],\n  \"method\": \"POST\",\n  \"bodyFormat\": \"json\",\n  \"payloadMode\": \"custom\",\n  \"payloadTemplate\": \"{\\n  \\\"event\\\": \\\"{{event.type}}\\\",\\n  \\\"renderId\\\": \\\"{{data.id}}\\\",\\n  \\\"status\\\": \\\"{{data.status}}\\\",\\n  \\\"fileUrl\\\": \\\"{{data.url}}\\\",\\n  \\\"creditsUsed\\\": \\\"{{data.credits}}\\\"\\n}\",\n  \"headers\": [\n    {\n      \"key\": \"Authorization\",\n      \"value\": \"Bearer your-receiver-token\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/organization/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "organization",
                "webhooks"
              ]
            },
            "description": "Registers an endpoint and returns its signing secret. The secret is in this response only — it is never readable again, so store it before moving on.\n\nBody parameters:\n- `url` (string) — required — HTTPS in production. May contain `{{variable}}` placeholders.\n- `events` (string[]) — required — render.queued, render.started, render.completed, render.failed, template.published, credits.low, credits.exhausted, subscription.updated, payment.captured.\n- `description` (string) — Free text, for your own reference.\n- `method` (string) — POST (default), PUT, PATCH or GET.\n- `bodyFormat` (string) — json (default), form, multipart or none. Forced to none for GET.\n- `payloadMode` (string) — default sends our event envelope; custom sends `payloadTemplate`.\n- `payloadTemplate` (string) — A JSON object, as text, holding `{{variable}}` placeholders. Required when payloadMode is custom, and rejected if it is not valid JSON.\n- `headers` (array) — Up to 20 `{ key, value }` pairs sent with every delivery. Values take variables. Our `X-Pixbix-*` signature headers cannot be overridden.\n\nVariables resolve per delivery: `{{event.type}}`, `{{event.id}}`, `{{event.created}}`, `{{timestamp}}`, `{{data}}` and any field within it (`{{data.id}}`, `{{data.url}}`, `{{data.status}}`, `{{data.credits}}`…), `{{org.id}}`, `{{endpoint.id}}`. An unknown path resolves to an empty string. In a JSON payload a value that is only a placeholder keeps its type, so `\"{{data}}\"` sends the object itself.\n\nNote that these placeholders share Postman's own `{{…}}` syntax. They are left alone as long as no environment variable of the same name exists, which is why none of them are declared in ours."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/organization/webhooks"
                }
              },
              "status": "OK",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"whe_7c1f9a\",\n    \"url\": \"https://api.acme.com/hooks/pixbix\",\n    \"description\": \"Production render handler\",\n    \"events\": [\n      \"render.completed\",\n      \"render.failed\",\n      \"render.cancelled\"\n    ],\n    \"method\": \"POST\",\n    \"bodyFormat\": \"json\",\n    \"payloadMode\": \"custom\",\n    \"payloadTemplate\": \"{\\n  \\\"event\\\": \\\"{{event.type}}\\\",\\n  \\\"renderId\\\": \\\"{{data.id}}\\\"\\n}\",\n    \"headers\": [\n      {\n        \"key\": \"Authorization\",\n        \"value\": \"Bearer your-receiver-token\"\n      }\n    ],\n    \"secretHint\": \"whsec_…8f2a\",\n    \"isActive\": true,\n    \"consecutiveFailures\": 0,\n    \"lastSuccessAt\": null,\n    \"createdAt\": \"2026-08-15T09:42:11.204Z\",\n    \"secret\": \"whsec_Q8vN2pR7yTt0mK4dLwXhAe6c\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Update a webhook endpoint",
          "request": {
            "method": "PATCH",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"events\": [\n    \"render.completed\"\n  ],\n  \"method\": \"PUT\",\n  \"bodyFormat\": \"form\",\n  \"payloadMode\": \"default\",\n  \"isActive\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/organization/webhooks/{{webhookId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "organization",
                "webhooks",
                "{{webhookId}}"
              ]
            },
            "description": "Changes only the fields you send, so a call that toggles `isActive` leaves the request configuration alone. Re-enabling a disabled endpoint clears its failure streak.\n\nAccepts every field of the create call, plus:\n- `isActive` (boolean) — Pause or resume deliveries.\n\nBody parameters:\n- `url` (string) — required — HTTPS in production. May contain `{{variable}}` placeholders.\n- `events` (string[]) — required — render.queued, render.started, render.completed, render.failed, template.published, credits.low, credits.exhausted, subscription.updated, payment.captured.\n- `description` (string) — Free text, for your own reference.\n- `method` (string) — POST (default), PUT, PATCH or GET.\n- `bodyFormat` (string) — json (default), form, multipart or none. Forced to none for GET.\n- `payloadMode` (string) — default sends our event envelope; custom sends `payloadTemplate`.\n- `payloadTemplate` (string) — A JSON object, as text, holding `{{variable}}` placeholders. Required when payloadMode is custom, and rejected if it is not valid JSON.\n- `headers` (array) — Up to 20 `{ key, value }` pairs sent with every delivery. Values take variables. Our `X-Pixbix-*` signature headers cannot be overridden.\n\nVariables resolve per delivery: `{{event.type}}`, `{{event.id}}`, `{{event.created}}`, `{{timestamp}}`, `{{data}}` and any field within it (`{{data.id}}`, `{{data.url}}`, `{{data.status}}`, `{{data.credits}}`…), `{{org.id}}`, `{{endpoint.id}}`. An unknown path resolves to an empty string. In a JSON payload a value that is only a placeholder keeps its type, so `\"{{data}}\"` sends the object itself.\n\nNote that these placeholders share Postman's own `{{…}}` syntax. They are left alone as long as no environment variable of the same name exists, which is why none of them are declared in ours."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "PATCH",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/organization/webhooks/{{webhookId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"whe_7c1f9a\",\n    \"url\": \"https://api.acme.com/hooks/pixbix\",\n    \"description\": \"Production render handler\",\n    \"events\": [\n      \"render.completed\",\n      \"render.failed\",\n      \"render.cancelled\"\n    ],\n    \"method\": \"PUT\",\n    \"bodyFormat\": \"form\",\n    \"payloadMode\": \"default\",\n    \"payloadTemplate\": \"{\\n  \\\"event\\\": \\\"{{event.type}}\\\",\\n  \\\"renderId\\\": \\\"{{data.id}}\\\"\\n}\",\n    \"headers\": [\n      {\n        \"key\": \"Authorization\",\n        \"value\": \"Bearer your-receiver-token\"\n      }\n    ],\n    \"secretHint\": \"whsec_…8f2a\",\n    \"isActive\": true,\n    \"consecutiveFailures\": 0,\n    \"lastSuccessAt\": null,\n    \"createdAt\": \"2026-08-15T09:42:11.204Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Send a test event",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/organization/webhooks/{{webhookId}}/test",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "organization",
                "webhooks",
                "{{webhookId}}",
                "test"
              ]
            },
            "description": "Delivers one sample `render.completed` event, built and signed exactly like a real one — so it exercises the configured method, encoding, headers and payload. Carries `X-Pixbix-Test: true`. A failed test does not count towards the auto-disable streak."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/organization/webhooks/{{webhookId}}/test"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"ok\": true,\n    \"status\": 200\n  }\n}"
            }
          ]
        },
        {
          "name": "Delete a webhook endpoint",
          "request": {
            "method": "DELETE",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/organization/webhooks/{{webhookId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "organization",
                "webhooks",
                "{{webhookId}}"
              ]
            },
            "description": "Deliveries stop immediately. Renders are unaffected — results remain readable from `GET /v1/render/:id`."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/organization/webhooks/{{webhookId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": null\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Workflows (workspace)",
      "description": "Build and edit saved automations. Authenticated with a dashboard session token plus `X-Pixbix-Org`, not with an API key — set `authToken` and `orgId` in the environment first. Running and reading workflows is in the metered v1 folder and takes an API key instead.",
      "item": [
        {
          "name": "List workflows",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/workflows",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows"
              ]
            },
            "description": "Every workflow in the workspace, most recently changed first. The inbound hook token is never returned."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"wfl_7c1f9a\",\n      \"name\": \"Daily product posters\",\n      \"isActive\": true,\n      \"trigger\": {\n        \"type\": \"schedule\",\n        \"cron\": \"0 6 * * *\",\n        \"timezone\": \"Asia/Kolkata\",\n        \"events\": [],\n        \"hookTokenHint\": \"…4f9c2a\"\n      },\n      \"scheduleDescription\": \"At 06:00 (Asia/Kolkata)\",\n      \"nextRunAt\": \"2026-08-16T00:30:00.000Z\",\n      \"maxItemsPerRun\": 100,\n      \"skipIfRunning\": true,\n      \"steps\": [\n        {\n          \"type\": \"source.sheet\",\n          \"name\": \"Read the catalogue\",\n          \"config\": {\n            \"spreadsheetId\": \"{{spreadsheetId}}\",\n            \"range\": \"Products!A1:F500\"\n          }\n        },\n        {\n          \"type\": \"filter\",\n          \"name\": \"In stock only\",\n          \"config\": {\n            \"match\": \"all\",\n            \"conditions\": [\n              {\n                \"left\": \"{{item.Stock}}\",\n                \"operator\": \"gt\",\n                \"right\": \"0\"\n              }\n            ]\n          }\n        },\n        {\n          \"type\": \"render.template\",\n          \"name\": \"Render the poster\",\n          \"config\": {\n            \"templateId\": \"{{id}}\",\n            \"waitForCompletion\": true,\n            \"values\": {\n              \"headline\": \"{{item.Product name}}\",\n              \"price\": \"{{item.Price | currency}}\",\n              \"product\": \"{{item.Image URL}}\"\n            }\n          }\n        },\n        {\n          \"type\": \"deliver.sheet\",\n          \"name\": \"Write the URLs back\",\n          \"config\": {\n            \"mode\": \"update\",\n            \"updates\": [\n              {\n                \"column\": \"Video URL\",\n                \"value\": \"{{item.render.url}}\"\n              },\n              {\n                \"column\": \"Rendered on\",\n                \"value\": \"{{now.date}}\"\n              }\n            ]\n          }\n        }\n      ],\n      \"stats\": {\n        \"runs\": 0,\n        \"succeeded\": 0,\n        \"failed\": 0,\n        \"itemsProcessed\": 0\n      },\n      \"createdAt\": \"2026-08-15T09:42:11.204Z\"\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Create a workflow",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Daily product posters\",\n  \"description\": \"Renders a poster for every in-stock row and writes the URLs back to the sheet.\",\n  \"trigger\": {\n    \"type\": \"schedule\",\n    \"cron\": \"0 6 * * *\",\n    \"timezone\": \"Asia/Kolkata\"\n  },\n  \"isActive\": true,\n  \"maxItemsPerRun\": 100000,\n  \"concurrency\": 20,\n  \"skipIfRunning\": true,\n  \"steps\": [\n    {\n      \"type\": \"source.sheet\",\n      \"name\": \"Read the catalogue\",\n      \"config\": {\n        \"spreadsheetId\": \"{{spreadsheetId}}\",\n        \"range\": \"Products!A1:F500\"\n      }\n    },\n    {\n      \"type\": \"filter\",\n      \"name\": \"In stock only\",\n      \"config\": {\n        \"match\": \"all\",\n        \"conditions\": [\n          {\n            \"left\": \"{{item.Stock}}\",\n            \"operator\": \"gt\",\n            \"right\": \"0\"\n          }\n        ]\n      }\n    },\n    {\n      \"type\": \"render.template\",\n      \"name\": \"Render the poster\",\n      \"config\": {\n        \"templateId\": \"{{id}}\",\n        \"waitForCompletion\": true,\n        \"values\": {\n          \"headline\": \"{{item.Product name}}\",\n          \"price\": \"{{item.Price | currency}}\",\n          \"product\": \"{{item.Image URL}}\"\n        }\n      }\n    },\n    {\n      \"type\": \"deliver.sheet\",\n      \"name\": \"Write the URLs back\",\n      \"config\": {\n        \"mode\": \"update\",\n        \"updates\": [\n          {\n            \"column\": \"Video URL\",\n            \"value\": \"{{item.render.url}}\"\n          },\n          {\n            \"column\": \"Rendered on\",\n            \"value\": \"{{now.date}}\"\n          }\n        ]\n      }\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/workflows",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows"
              ]
            },
            "description": "Creates the workflow and, for a schedule, computes when it first fires. A workflow with no steps is accepted as a draft but cannot be activated — an active workflow with nothing to do would burn a run of your monthly allowance every hour to accomplish nothing.\n\nBody parameters:\n- `name` (string) — required — What it is called.\n- `description` (string) — For whoever opens it in six months.\n- `trigger.type` (string) — manual, schedule, event, hook or poll.\n- `trigger.cron` (string) — Five fields, minute-resolution. Required for schedule.\n- `trigger.timezone` (string) — IANA zone the cron is read in, through daylight saving.\n- `trigger.events` (string[]) — For event triggers: the same event names webhooks deliver.\n- `steps` (array) — required — Ordered. Each is `{ type, name, config, continueOnError, isEnabled }`. How many you may have is a plan entitlement (`limits.workflowMaxSteps`): over it, the save is refused with 402 `LIMIT_WORKFLOWMAXSTEPS` and the message names your allowance. The same cap is re-checked when a run starts, so a downgrade stops an over-long workflow rather than silently running part of it.\n- `isActive` (boolean) — Switches it on. A workflow with no steps cannot be activated.\n- `maxItemsPerRun` (number) — 1–1,000,000. Hard ceiling on rows per run, whatever the source returned.\n- `concurrency` (number) — 1–100. Rows worked in parallel, clamped at run time to your plan's `concurrentRenders`. This is the setting that decides how long a large run takes.\n- `trigger.everyMinutes` (number) — 1–1440. For `poll`: how often to check for new rows.\n- `skipIfRunning` (boolean) — Refuse to start while the previous run is still going. Leave it on.\n\nStep types: `source.sheet`, `source.drive`, `source.contacts`, `source.whatsapp`, `source.http`, `source.list`, `filter`, `map`, `limit`, `delay`, `dedupe`, `route`, `screenshot.url`, `audio.voiceover`, `drive.import`, `http.request`, `render.template`, `deliver.sheet`, `deliver.webhook`, `deliver.drive`, `deliver.whatsapp`, `deliver.email`.\n\nGOOGLE DRIVE. `source.drive` gives one row per file in `folderId`, optionally narrowed by `kind` (image/video/audio) and `search`; with a `poll` trigger it lists only files modified since the last check. A row carries the file's details, not its bytes — `drive.import` downloads it and leaves `{{item.file.url}}`, which a render can actually read. `deliver.drive` takes `folderPath`, a template created on demand and reused when it exists, so `Renders/{{now.date}}` files output by day.\n\nWHATSAPP. `source.whatsapp` gives one row per contact on the connected MotherBot account, narrowed by `tag`; contacts who opted out or are blocked never reach the run, so nothing is rendered for somebody who cannot be messaged. `deliver.whatsapp` sends that row's own render back to that row's number in an approved template — `to` (defaults to `{{item.phone}}`), `templateName`, `mediaUrl` and `variables`, keyed by the template's numbered `{{1}}` slots. `mediaUrl` is an ordinary templated value, so it takes `{{item.render.url}}`, `{{item.screenshot.url}}`, `{{item.file.url}}` or any column already holding a URL; leave it empty for a template with no media. The language is the template's own and is never configured on the step. The file type must match what the template was approved for; `GET /v1/integrations/motherbot/templates` reports `headerFormat` for each. Sends are billed as conversations on your own MotherBot plan and cost no pixbix credits.\n\nSteps run in one of three phases, decided by POSITION. Everything before the first step that produces something (`render.template`, `screenshot.url`, `audio.voiceover`) is applied while rows stream out of the source, so a filter above the render never creates tasks for rows it drops. Everything after runs per row, concurrently and resumably. `deliver.email`, and `deliver.webhook` in `batch` mode, run once at the end over counts and a sample.\n\nSOURCES FETCH CONDITIONALLY. `source.sheet`, `source.http` and `source.list` all take `where` (conditions checked as the source is read) and `whereMatch` (all/any). `source.sheet` also takes `onlyMissing` — a column name — which keeps only rows where that column is still empty, and `stopAfter`, which stops reading once enough have matched. Rows that do not match are never fetched, never queued and never rendered, so a sheet of 50,000 whose condition matches 40 costs 40 renders.\n\n`source.http` pages: set `pagination` to `page`, `offset` or `cursor`, plus `pageParam` / `cursorParam` / `cursorPath`, `sizeParam`, `pageSize` and `maxPages`. All three stop on an empty page and are bounded.\n\n`screenshot.url` takes a `presetId` from your saved capture presets; anything in `options` refines it rather than being overridden by it.\n\n`route` sets a field from the first matching rule — name that field where a `templateId` goes and one workflow renders a different creative per segment. `dedupe` drops rows already handled, which is what makes a `poll` trigger safe to repeat. `http.request` calls your API per row and leaves the reply on it.\n\n`deliver.sheet` takes `mode`: `update` (default) fills named columns on the row each result came from, using `updates: [{ column, value }]` — the spreadsheet and row travel on the row from the source step. `append` adds new rows instead and takes `columns: []`.\n\n`audio.voiceover` speaks a templated `text` through your ElevenLabs connection once per row and leaves `{{item.voice.url}}` and `{{item.voice.script}}` on it. Characters come off your ElevenLabs plan.\n\nEvery string in a step's `config` is a template. `{{item.COLUMN}}` is the row being worked on, `{{item.render.url}}`, `{{item.screenshot.url}}` and `{{item.voice.url}}` are what the work steps left behind, `{{item._row}}` is the spreadsheet row it came from, `{{number}}` is the 1-based row number, and `{{now.date}}`, `{{workflow.name}}`, `{{run.id}}` and `{{trigger.payload.FIELD}}` are available throughout. Values take a formatter after a pipe: `{{item.price | currency}}`.\n\nThese placeholders share Postman's own `{{…}}` syntax and are left alone as long as no environment variable of the same name exists — which is why none of them are declared in ours."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows"
                }
              },
              "status": "OK",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"wfl_7c1f9a\",\n    \"name\": \"Daily product posters\",\n    \"isActive\": true,\n    \"trigger\": {\n      \"type\": \"schedule\",\n      \"cron\": \"0 6 * * *\",\n      \"timezone\": \"Asia/Kolkata\",\n      \"events\": [],\n      \"hookTokenHint\": \"…4f9c2a\"\n    },\n    \"scheduleDescription\": \"At 06:00 (Asia/Kolkata)\",\n    \"nextRunAt\": \"2026-08-16T00:30:00.000Z\",\n    \"maxItemsPerRun\": 100,\n    \"skipIfRunning\": true,\n    \"steps\": [\n      {\n        \"type\": \"source.sheet\",\n        \"name\": \"Read the catalogue\",\n        \"config\": {\n          \"spreadsheetId\": \"{{spreadsheetId}}\",\n          \"range\": \"Products!A1:F500\"\n        }\n      },\n      {\n        \"type\": \"filter\",\n        \"name\": \"In stock only\",\n        \"config\": {\n          \"match\": \"all\",\n          \"conditions\": [\n            {\n              \"left\": \"{{item.Stock}}\",\n              \"operator\": \"gt\",\n              \"right\": \"0\"\n            }\n          ]\n        }\n      },\n      {\n        \"type\": \"render.template\",\n        \"name\": \"Render the poster\",\n        \"config\": {\n          \"templateId\": \"{{id}}\",\n          \"waitForCompletion\": true,\n          \"values\": {\n            \"headline\": \"{{item.Product name}}\",\n            \"price\": \"{{item.Price | currency}}\",\n            \"product\": \"{{item.Image URL}}\"\n          }\n        }\n      },\n      {\n        \"type\": \"deliver.sheet\",\n        \"name\": \"Write the URLs back\",\n        \"config\": {\n          \"mode\": \"update\",\n          \"updates\": [\n            {\n              \"column\": \"Video URL\",\n              \"value\": \"{{item.render.url}}\"\n            },\n            {\n              \"column\": \"Rendered on\",\n              \"value\": \"{{now.date}}\"\n            }\n          ]\n        }\n      }\n    ],\n    \"stats\": {\n      \"runs\": 0,\n      \"succeeded\": 0,\n      \"failed\": 0,\n      \"itemsProcessed\": 0\n    },\n    \"createdAt\": \"2026-08-15T09:42:11.204Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Get a workflow",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/workflows/{{workflowId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows",
                "{{workflowId}}"
              ]
            },
            "description": "One workflow in full. This is the only response that carries `trigger.hookUrl` — the inbound URL is a credential, so it is returned where it was explicitly asked for and nowhere else."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows/{{workflowId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"wfl_7c1f9a\",\n    \"name\": \"Daily product posters\",\n    \"isActive\": true,\n    \"trigger\": {\n      \"type\": \"hook\",\n      \"hookUrl\": \"https://pixbix.app/api/hooks/workflows/whk_9tK2…\"\n    },\n    \"scheduleDescription\": \"At 06:00 (Asia/Kolkata)\",\n    \"nextRunAt\": \"2026-08-16T00:30:00.000Z\",\n    \"maxItemsPerRun\": 100,\n    \"skipIfRunning\": true,\n    \"steps\": [\n      {\n        \"type\": \"source.sheet\",\n        \"name\": \"Read the catalogue\",\n        \"config\": {\n          \"spreadsheetId\": \"{{spreadsheetId}}\",\n          \"range\": \"Products!A1:F500\"\n        }\n      },\n      {\n        \"type\": \"filter\",\n        \"name\": \"In stock only\",\n        \"config\": {\n          \"match\": \"all\",\n          \"conditions\": [\n            {\n              \"left\": \"{{item.Stock}}\",\n              \"operator\": \"gt\",\n              \"right\": \"0\"\n            }\n          ]\n        }\n      },\n      {\n        \"type\": \"render.template\",\n        \"name\": \"Render the poster\",\n        \"config\": {\n          \"templateId\": \"{{id}}\",\n          \"waitForCompletion\": true,\n          \"values\": {\n            \"headline\": \"{{item.Product name}}\",\n            \"price\": \"{{item.Price | currency}}\",\n            \"product\": \"{{item.Image URL}}\"\n          }\n        }\n      },\n      {\n        \"type\": \"deliver.sheet\",\n        \"name\": \"Write the URLs back\",\n        \"config\": {\n          \"mode\": \"update\",\n          \"updates\": [\n            {\n              \"column\": \"Video URL\",\n              \"value\": \"{{item.render.url}}\"\n            },\n            {\n              \"column\": \"Rendered on\",\n              \"value\": \"{{now.date}}\"\n            }\n          ]\n        }\n      }\n    ],\n    \"stats\": {\n      \"runs\": 0,\n      \"succeeded\": 0,\n      \"failed\": 0,\n      \"itemsProcessed\": 0\n    },\n    \"createdAt\": \"2026-08-15T09:42:11.204Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Update a workflow",
          "request": {
            "method": "PATCH",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"isActive\": false,\n  \"maxItemsPerRun\": 250\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/workflows/{{workflowId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows",
                "{{workflowId}}"
              ]
            },
            "description": "Changes only the fields you send, so a call that pauses a workflow leaves its steps alone. Sending `steps` replaces the whole list. The next scheduled run is recomputed on every save, because switching a workflow on or moving its timezone changes when it fires just as much as editing the expression does.\n\nBody parameters:\n- `name` (string) — required — What it is called.\n- `description` (string) — For whoever opens it in six months.\n- `trigger.type` (string) — manual, schedule, event, hook or poll.\n- `trigger.cron` (string) — Five fields, minute-resolution. Required for schedule.\n- `trigger.timezone` (string) — IANA zone the cron is read in, through daylight saving.\n- `trigger.events` (string[]) — For event triggers: the same event names webhooks deliver.\n- `steps` (array) — required — Ordered. Each is `{ type, name, config, continueOnError, isEnabled }`. How many you may have is a plan entitlement (`limits.workflowMaxSteps`): over it, the save is refused with 402 `LIMIT_WORKFLOWMAXSTEPS` and the message names your allowance. The same cap is re-checked when a run starts, so a downgrade stops an over-long workflow rather than silently running part of it.\n- `isActive` (boolean) — Switches it on. A workflow with no steps cannot be activated.\n- `maxItemsPerRun` (number) — 1–1,000,000. Hard ceiling on rows per run, whatever the source returned.\n- `concurrency` (number) — 1–100. Rows worked in parallel, clamped at run time to your plan's `concurrentRenders`. This is the setting that decides how long a large run takes.\n- `trigger.everyMinutes` (number) — 1–1440. For `poll`: how often to check for new rows.\n- `skipIfRunning` (boolean) — Refuse to start while the previous run is still going. Leave it on.\n\nStep types: `source.sheet`, `source.drive`, `source.contacts`, `source.whatsapp`, `source.http`, `source.list`, `filter`, `map`, `limit`, `delay`, `dedupe`, `route`, `screenshot.url`, `audio.voiceover`, `drive.import`, `http.request`, `render.template`, `deliver.sheet`, `deliver.webhook`, `deliver.drive`, `deliver.whatsapp`, `deliver.email`.\n\nGOOGLE DRIVE. `source.drive` gives one row per file in `folderId`, optionally narrowed by `kind` (image/video/audio) and `search`; with a `poll` trigger it lists only files modified since the last check. A row carries the file's details, not its bytes — `drive.import` downloads it and leaves `{{item.file.url}}`, which a render can actually read. `deliver.drive` takes `folderPath`, a template created on demand and reused when it exists, so `Renders/{{now.date}}` files output by day.\n\nWHATSAPP. `source.whatsapp` gives one row per contact on the connected MotherBot account, narrowed by `tag`; contacts who opted out or are blocked never reach the run, so nothing is rendered for somebody who cannot be messaged. `deliver.whatsapp` sends that row's own render back to that row's number in an approved template — `to` (defaults to `{{item.phone}}`), `templateName`, `mediaUrl` and `variables`, keyed by the template's numbered `{{1}}` slots. `mediaUrl` is an ordinary templated value, so it takes `{{item.render.url}}`, `{{item.screenshot.url}}`, `{{item.file.url}}` or any column already holding a URL; leave it empty for a template with no media. The language is the template's own and is never configured on the step. The file type must match what the template was approved for; `GET /v1/integrations/motherbot/templates` reports `headerFormat` for each. Sends are billed as conversations on your own MotherBot plan and cost no pixbix credits.\n\nSteps run in one of three phases, decided by POSITION. Everything before the first step that produces something (`render.template`, `screenshot.url`, `audio.voiceover`) is applied while rows stream out of the source, so a filter above the render never creates tasks for rows it drops. Everything after runs per row, concurrently and resumably. `deliver.email`, and `deliver.webhook` in `batch` mode, run once at the end over counts and a sample.\n\nSOURCES FETCH CONDITIONALLY. `source.sheet`, `source.http` and `source.list` all take `where` (conditions checked as the source is read) and `whereMatch` (all/any). `source.sheet` also takes `onlyMissing` — a column name — which keeps only rows where that column is still empty, and `stopAfter`, which stops reading once enough have matched. Rows that do not match are never fetched, never queued and never rendered, so a sheet of 50,000 whose condition matches 40 costs 40 renders.\n\n`source.http` pages: set `pagination` to `page`, `offset` or `cursor`, plus `pageParam` / `cursorParam` / `cursorPath`, `sizeParam`, `pageSize` and `maxPages`. All three stop on an empty page and are bounded.\n\n`screenshot.url` takes a `presetId` from your saved capture presets; anything in `options` refines it rather than being overridden by it.\n\n`route` sets a field from the first matching rule — name that field where a `templateId` goes and one workflow renders a different creative per segment. `dedupe` drops rows already handled, which is what makes a `poll` trigger safe to repeat. `http.request` calls your API per row and leaves the reply on it.\n\n`deliver.sheet` takes `mode`: `update` (default) fills named columns on the row each result came from, using `updates: [{ column, value }]` — the spreadsheet and row travel on the row from the source step. `append` adds new rows instead and takes `columns: []`.\n\n`audio.voiceover` speaks a templated `text` through your ElevenLabs connection once per row and leaves `{{item.voice.url}}` and `{{item.voice.script}}` on it. Characters come off your ElevenLabs plan.\n\nEvery string in a step's `config` is a template. `{{item.COLUMN}}` is the row being worked on, `{{item.render.url}}`, `{{item.screenshot.url}}` and `{{item.voice.url}}` are what the work steps left behind, `{{item._row}}` is the spreadsheet row it came from, `{{number}}` is the 1-based row number, and `{{now.date}}`, `{{workflow.name}}`, `{{run.id}}` and `{{trigger.payload.FIELD}}` are available throughout. Values take a formatter after a pipe: `{{item.price | currency}}`.\n\nThese placeholders share Postman's own `{{…}}` syntax and are left alone as long as no environment variable of the same name exists — which is why none of them are declared in ours."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "PATCH",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows/{{workflowId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"wfl_7c1f9a\",\n    \"name\": \"Daily product posters\",\n    \"isActive\": false,\n    \"trigger\": {\n      \"type\": \"schedule\",\n      \"cron\": \"0 6 * * *\",\n      \"timezone\": \"Asia/Kolkata\",\n      \"events\": [],\n      \"hookTokenHint\": \"…4f9c2a\"\n    },\n    \"scheduleDescription\": \"At 06:00 (Asia/Kolkata)\",\n    \"nextRunAt\": \"2026-08-16T00:30:00.000Z\",\n    \"maxItemsPerRun\": 250,\n    \"skipIfRunning\": true,\n    \"steps\": [\n      {\n        \"type\": \"source.sheet\",\n        \"name\": \"Read the catalogue\",\n        \"config\": {\n          \"spreadsheetId\": \"{{spreadsheetId}}\",\n          \"range\": \"Products!A1:F500\"\n        }\n      },\n      {\n        \"type\": \"filter\",\n        \"name\": \"In stock only\",\n        \"config\": {\n          \"match\": \"all\",\n          \"conditions\": [\n            {\n              \"left\": \"{{item.Stock}}\",\n              \"operator\": \"gt\",\n              \"right\": \"0\"\n            }\n          ]\n        }\n      },\n      {\n        \"type\": \"render.template\",\n        \"name\": \"Render the poster\",\n        \"config\": {\n          \"templateId\": \"{{id}}\",\n          \"waitForCompletion\": true,\n          \"values\": {\n            \"headline\": \"{{item.Product name}}\",\n            \"price\": \"{{item.Price | currency}}\",\n            \"product\": \"{{item.Image URL}}\"\n          }\n        }\n      },\n      {\n        \"type\": \"deliver.sheet\",\n        \"name\": \"Write the URLs back\",\n        \"config\": {\n          \"mode\": \"update\",\n          \"updates\": [\n            {\n              \"column\": \"Video URL\",\n              \"value\": \"{{item.render.url}}\"\n            },\n            {\n              \"column\": \"Rendered on\",\n              \"value\": \"{{now.date}}\"\n            }\n          ]\n        }\n      }\n    ],\n    \"stats\": {\n      \"runs\": 0,\n      \"succeeded\": 0,\n      \"failed\": 0,\n      \"itemsProcessed\": 0\n    },\n    \"createdAt\": \"2026-08-15T09:42:11.204Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Rotate the inbound hook URL",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/workflows/{{workflowId}}/rotate-hook",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows",
                "{{workflowId}}",
                "rotate-hook"
              ]
            },
            "description": "Issues a new hook token and returns the new URL. The old URL stops working immediately — the reason to rotate is that it leaked, so there is no grace period."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows/{{workflowId}}/rotate-hook"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"wfl_7c1f9a\",\n    \"name\": \"Daily product posters\",\n    \"isActive\": true,\n    \"trigger\": {\n      \"type\": \"hook\",\n      \"hookUrl\": \"https://pixbix.app/api/hooks/workflows/whk_new…\"\n    },\n    \"scheduleDescription\": \"At 06:00 (Asia/Kolkata)\",\n    \"nextRunAt\": \"2026-08-16T00:30:00.000Z\",\n    \"maxItemsPerRun\": 100,\n    \"skipIfRunning\": true,\n    \"steps\": [\n      {\n        \"type\": \"source.sheet\",\n        \"name\": \"Read the catalogue\",\n        \"config\": {\n          \"spreadsheetId\": \"{{spreadsheetId}}\",\n          \"range\": \"Products!A1:F500\"\n        }\n      },\n      {\n        \"type\": \"filter\",\n        \"name\": \"In stock only\",\n        \"config\": {\n          \"match\": \"all\",\n          \"conditions\": [\n            {\n              \"left\": \"{{item.Stock}}\",\n              \"operator\": \"gt\",\n              \"right\": \"0\"\n            }\n          ]\n        }\n      },\n      {\n        \"type\": \"render.template\",\n        \"name\": \"Render the poster\",\n        \"config\": {\n          \"templateId\": \"{{id}}\",\n          \"waitForCompletion\": true,\n          \"values\": {\n            \"headline\": \"{{item.Product name}}\",\n            \"price\": \"{{item.Price | currency}}\",\n            \"product\": \"{{item.Image URL}}\"\n          }\n        }\n      },\n      {\n        \"type\": \"deliver.sheet\",\n        \"name\": \"Write the URLs back\",\n        \"config\": {\n          \"mode\": \"update\",\n          \"updates\": [\n            {\n              \"column\": \"Video URL\",\n              \"value\": \"{{item.render.url}}\"\n            },\n            {\n              \"column\": \"Rendered on\",\n              \"value\": \"{{now.date}}\"\n            }\n          ]\n        }\n      }\n    ],\n    \"stats\": {\n      \"runs\": 0,\n      \"succeeded\": 0,\n      \"failed\": 0,\n      \"itemsProcessed\": 0\n    },\n    \"createdAt\": \"2026-08-15T09:42:11.204Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Read the last inbound hook call",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/workflows/{{workflowId}}/hook-sample",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows",
                "{{workflowId}}",
                "hook-sample"
              ]
            },
            "description": "What the hook last received, flattened into the paths a step can reference. This is what makes a `hook` trigger configurable: fire one call from the other system BEFORE building the steps, and every field comes back as a ready-to-paste `{{trigger.payload.…}}` token with the value that arrived beside it.\n\nCalls are captured whether or not the workflow is switched on, so the intended order is: set the trigger up, send one call, then build against what actually arrived. Only the most recent call is kept — this is a shape sample, not an audit log; runs are the record of what happened.\n\nHeaders are deliberately NOT stored. A hook URL is commonly pasted into a third party that also sends an Authorization header, and keeping those on a document the whole workspace can read would turn a convenience into a credential leak."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows/{{workflowId}}/hook-sample"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"capturedAt\": \"2026-08-16T09:42:11.000Z\",\n    \"method\": \"POST\",\n    \"payload\": {\n      \"customer\": {\n        \"email\": \"priya@acme.in\",\n        \"firstName\": \"Priya\"\n      },\n      \"orderId\": \"ord_8812\"\n    },\n    \"fields\": [\n      {\n        \"token\": \"{{trigger.payload.customer.email}}\",\n        \"path\": \"customer.email\",\n        \"sample\": \"priya@acme.in\"\n      },\n      {\n        \"token\": \"{{trigger.payload.customer.firstName}}\",\n        \"path\": \"customer.firstName\",\n        \"sample\": \"Priya\"\n      },\n      {\n        \"token\": \"{{trigger.payload.orderId}}\",\n        \"path\": \"orderId\",\n        \"sample\": \"ord_8812\"\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Set the hook security",
          "request": {
            "method": "PUT",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"secret\": true,\n  \"signingSecret\": true,\n  \"toleranceSec\": 300,\n  \"allowedIps\": [\n    \"203.0.113.0/24\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/workflows/{{workflowId}}/hook-security",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows",
                "{{workflowId}}",
                "hook-security"
              ]
            },
            "description": "Optional hardening on top of the URL, all off until set. Send `true` for a secret to have one generated — most people should not be inventing their own — or a string of at least 16 characters to supply your own. Send `null` to clear one; OMIT the key to leave it untouched, because \"do not change the signing secret\" and \"remove it\" are both things somebody means.\n\nA generated secret is returned ONCE, in `issued`, and never again — the stored copy exists only to compare against, and reads report whether one exists rather than what it is.\n\nCallers then send `X-Pixbix-Token` (shared secret), `X-Pixbix-Signature` (HMAC-SHA256 over the raw body, `sha256=` prefix optional) and `X-Pixbix-Timestamp` (checked against `toleranceSec`; 0 disables the replay window). `allowedIps` accepts addresses and CIDR ranges."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "PUT",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows/{{workflowId}}/hook-security"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"workflow\": {\n      \"id\": \"wfl_7c1f9a\",\n      \"trigger\": {\n        \"type\": \"hook\",\n        \"security\": {\n          \"hasSecret\": true,\n          \"hasSigningSecret\": true,\n          \"toleranceSec\": 300,\n          \"allowedIps\": [\n            \"203.0.113.0/24\"\n          ]\n        }\n      }\n    },\n    \"issued\": {\n      \"secret\": \"whs_9tK2…\",\n      \"signingSecret\": \"whs_4bQ7…\"\n    }\n  }\n}"
            }
          ]
        },
        {
          "name": "Test a step",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"step\": {\n    \"type\": \"source.sheet\",\n    \"name\": \"Read the contacts\",\n    \"config\": {\n      \"spreadsheetId\": \"{{spreadsheetId}}\",\n      \"tab\": \"Contacts\",\n      \"onlyMissing\": \"Video URL\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/workflows/{{workflowId}}/sample",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows",
                "{{workflowId}}",
                "sample"
              ]
            },
            "description": "Runs ONE step as sent — not as saved — and reports the fields it produced, with the values they held. This is how you find out what an endpoint actually returns instead of guessing a path: the reply is flattened into real placeholders like `{{item.response.customer.name}}`.\n\nFree and side-effect-free. It reads sources and calls read-shaped endpoints; it never renders, never spends credits and never writes to a spreadsheet. Only `source.*` and `http.request` can be tested — steps that would cost money declare their output statically instead, which is why `{{item.render.url}}` is known without running anything.\n\nBody parameters:\n- `step` (object) — required — The step to test, in the same shape `steps[]` takes.\n- `rows` (array) — Optional. Rows from an earlier test, so `{{item.Email}}` in a URL resolves to a real value."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows/{{workflowId}}/sample"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"rows\": [\n      {\n        \"First name\": \"Priya\",\n        \"Email\": \"priya@acme.com\",\n        \"Video URL\": \"\",\n        \"_row\": 2\n      }\n    ],\n    \"fields\": [\n      {\n        \"path\": \"First name\",\n        \"token\": \"{{item.First name}}\",\n        \"label\": \"First name\",\n        \"sample\": \"Priya\",\n        \"type\": \"string\"\n      },\n      {\n        \"path\": \"Email\",\n        \"token\": \"{{item.Email}}\",\n        \"label\": \"Email\",\n        \"sample\": \"priya@acme.com\",\n        \"type\": \"string\"\n      }\n    ],\n    \"count\": null,\n    \"message\": \"3 fields found.\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Preview a schedule",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/workflows/schedule/preview?cron=0 6 * * 1-5&timezone=Asia/Kolkata",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows",
                "schedule",
                "preview?cron=0 6 * * 1-5&timezone=Asia/Kolkata"
              ]
            },
            "description": "What a cron expression means and the next five times it fires, from the same parser the scheduler runs. A schedule is the one setting you cannot verify by looking at the result — you find out it was wrong a day late — so check it here before switching the workflow on."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows/schedule/preview?cron=0 6 * * 1-5&timezone=Asia/Kolkata"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"cron\": \"0 6 * * 1-5\",\n    \"timezone\": \"Asia/Kolkata\",\n    \"description\": \"At 06:00 on Monday, Tuesday, Wednesday, Thursday and Friday (Asia/Kolkata)\",\n    \"upcoming\": [\n      \"2026-08-17T00:30:00.000Z\",\n      \"2026-08-18T00:30:00.000Z\"\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Cancel a run",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/workflows/runs/{{runId}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows",
                "runs",
                "{{runId}}",
                "cancel"
              ]
            },
            "description": "Stops a run that is still going, which is also what releases the \"skip if running\" guard so the workflow can fire again. Renders already dispatched keep going and settle normally — they are paid for, and the output is still yours."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows/runs/{{runId}}/cancel"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"wfr_2b8e41\",\n    \"status\": \"cancelled\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Delete a workflow",
          "request": {
            "method": "DELETE",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the call is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/workflows/{{workflowId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "workflows",
                "{{workflowId}}"
              ]
            },
            "description": "It stops firing immediately. The run history is kept — that is the record of credits already spent and files already produced, and deleting the recipe says nothing about what it did."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/workflows/{{workflowId}}"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": null\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Workflow hooks (no auth)",
      "description": "The catch-hook a workflow with a `hook` trigger listens on. Unauthenticated by design: the URL is the credential, which is what lets a system that knows nothing about pixbix start an automation. Read the URL from the workflow detail response, set it as `hookUrl` in your environment, and rotate it if it leaks.\n\nEVERY METHOD WORKS — GET, POST, PUT and PATCH. A write method's JSON body becomes the payload; a GET's query string does, so `?orderId=1234` arrives as `{{trigger.payload.orderId}}` exactly as a posted body would.\n\nSEND ONE CALL BEFORE YOU BUILD THE STEPS. Whatever arrives is recorded — whether or not the workflow is switched on — and the builder reads the field names out of it, so every value dropdown offers real paths with real values instead of asking somebody to type `{{trigger.payload.customer.email}}` from memory.\n\nOPTIONAL HARDENING, all off by default and layered on top of the token: a shared secret in `X-Pixbix-Token`, an HMAC-SHA256 signature over the raw body in `X-Pixbix-Signature` (a `sha256=` prefix is accepted), a replay window against `X-Pixbix-Timestamp`, and an IP/CIDR allowlist. Configure them with `PUT /workflows/:id/hook-security`. Every rejection returns the same 401 and the same body whatever failed, so probing a leaked URL yields no map of what to try next.",
      "item": [
        {
          "name": "Trigger a workflow",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"campaign\": \"diwali-2026\",\n  \"region\": \"west\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/hooks/workflows/{{hookToken}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "hooks",
                "workflows",
                "{{hookToken}}"
              ]
            },
            "description": "Starts a run and answers immediately with its id, so your timeout can never abandon a run halfway. The whole body arrives in every step as `{{trigger.payload.…}}` — the example here reaches a render step as `{{trigger.payload.campaign}}`.\n\nLimited to 60 calls a minute per token. Rate limiting is keyed on the token rather than the caller's address because the callers are somebody else's servers, on shared and rotating addresses, while the token identifies exactly one workspace's automation.\n\nAnswers: `202` when a run started, `202` with `started:false` when the workflow is switched off (the call is still captured for the builder, so a sender that disables a webhook after repeated errors is not told it failed), `401` when optional hook security rejects it, and `404` for an unknown token — deliberately indistinguishable from a token that exists but belongs elsewhere."
          },
          "response": [
            {
              "name": "Captured, but the workflow is off",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/hooks/workflows/{{hookToken}}"
                }
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Received and captured — this workflow is switched off, so nothing ran. Its fields are now available in the builder.\",\n  \"data\": {\n    \"workflowId\": \"wfl_7c1f9a\",\n    \"captured\": true,\n    \"started\": false\n  }\n}"
            },
            {
              "name": "Started",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/hooks/workflows/{{hookToken}}"
                }
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Workflow started\",\n  \"data\": {\n    \"runId\": \"wfr_2b8e41\",\n    \"status\": \"running\",\n    \"workflowId\": \"wfl_7c1f9a\"\n  }\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Email notifications (account)",
      "description": "Which operational emails an account receives. Authenticated with a dashboard session token — set `authToken` in the environment first.\n\nPER PERSON, NOT PER WORKSPACE. Someone who belongs to three workspaces answers once, and the answer follows them into all three — which is why no `X-Pixbix-Org` header is sent on these. The history endpoint is the exception: what we sent is a fact about a workspace, so it takes the header and the `org:read` permission.\n\nWorkspace-level alerts (a failed workflow, a dead integration, a bounced payment) go to the owner and admins, plus the billing role for anything about money. A member with another role can switch them on and still not receive them.",
      "item": [
        {
          "name": "Read notification preferences",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/notifications/preferences",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "notifications",
                "preferences"
              ]
            },
            "description": "The whole catalogue with this account’s answers applied — label, description, who each one goes to, its default and its current value. Render your settings screen from this rather than from a hard-coded list, and a notification added on the server appears without a client release.\n\nKeys, all booleans:\n- `render_failed` — a queued image or video did not complete.\n- `render_completed` — a long render finished (quick ones never send one).\n- `workflow_failed` — an unattended automation failed. One per failure streak.\n- `screenshot_failed` — captures started erroring. At most one an hour.\n- `api_failed` — your key received a 5xx from us. At most one an hour per endpoint.\n- `webhook_failed` — deliveries to one of your URLs are failing, and again if it is auto-disabled.\n- `integration_disconnected` — a connected account expired or was revoked.\n- `transcription_failed` — a caption job gave up after its retries.\n- `payment_failed` — the gateway could not collect.\n- `credits_low` — the image or video balance is nearly, or entirely, gone.\n- `usage_warning` — a monthly plan allowance passed 80%.\n\nKeys absent from an account follow the catalogue default rather than being written on save, so a notification added later still reaches accounts that set their preferences before it existed."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/notifications/preferences"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"email\": \"priya@company.com\",\n    \"emailVerified\": true,\n    \"groups\": [\n      {\n        \"id\": \"failures\",\n        \"title\": \"When something breaks\",\n        \"description\": \"Unattended work that stopped producing.\"\n      }\n    ],\n    \"types\": [\n      {\n        \"key\": \"workflow_failed\",\n        \"group\": \"failures\",\n        \"label\": \"A workflow stopped working\",\n        \"description\": \"A scheduled, event or hook-triggered automation failed, so it produced nothing.\",\n        \"audience\": \"workspace\",\n        \"defaultEnabled\": true,\n        \"enabled\": true\n      },\n      {\n        \"key\": \"render_completed\",\n        \"group\": \"activity\",\n        \"label\": \"A long render finished\",\n        \"description\": \"A link to the finished output in your dashboard, for a video that took long enough that you had walked away.\",\n        \"audience\": \"actor\",\n        \"defaultEnabled\": true,\n        \"enabled\": false\n      }\n    ],\n    \"alwaysSent\": [\n      {\n        \"label\": \"Email confirmation links\",\n        \"description\": \"Sent when you add or change an address.\"\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Update notification preferences",
          "request": {
            "method": "PUT",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"preferences\": {\n    \"render_completed\": false,\n    \"workflow_failed\": true\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/notifications/preferences",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "notifications",
                "preferences"
              ]
            },
            "description": "Send only the switches you are changing — this is a merge, not a replacement. Sending the full set would write an explicit value for every key and freeze this account’s future notifications at today’s defaults.\n\nAn unknown key is a 422 rather than being ignored, so a typo cannot become a preference that stores fine and changes nothing. Returns the same payload as the GET.\n\nKeys, all booleans:\n- `render_failed` — a queued image or video did not complete.\n- `render_completed` — a long render finished (quick ones never send one).\n- `workflow_failed` — an unattended automation failed. One per failure streak.\n- `screenshot_failed` — captures started erroring. At most one an hour.\n- `api_failed` — your key received a 5xx from us. At most one an hour per endpoint.\n- `webhook_failed` — deliveries to one of your URLs are failing, and again if it is auto-disabled.\n- `integration_disconnected` — a connected account expired or was revoked.\n- `transcription_failed` — a caption job gave up after its retries.\n- `payment_failed` — the gateway could not collect.\n- `credits_low` — the image or video balance is nearly, or entirely, gone.\n- `usage_warning` — a monthly plan allowance passed 80%.\n\nKeys absent from an account follow the catalogue default rather than being written on save, so a notification added later still reaches accounts that set their preferences before it existed."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "PUT",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/notifications/preferences"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Notification preferences saved\",\n  \"data\": {\n    \"email\": \"priya@company.com\",\n    \"emailVerified\": true,\n    \"groups\": [\n      {\n        \"id\": \"failures\",\n        \"title\": \"When something breaks\",\n        \"description\": \"Unattended work that stopped producing.\"\n      }\n    ],\n    \"types\": [\n      {\n        \"key\": \"workflow_failed\",\n        \"group\": \"failures\",\n        \"label\": \"A workflow stopped working\",\n        \"description\": \"A scheduled, event or hook-triggered automation failed, so it produced nothing.\",\n        \"audience\": \"workspace\",\n        \"defaultEnabled\": true,\n        \"enabled\": true\n      },\n      {\n        \"key\": \"render_completed\",\n        \"group\": \"activity\",\n        \"label\": \"A long render finished\",\n        \"description\": \"A link to the finished output in your dashboard, for a video that took long enough that you had walked away.\",\n        \"audience\": \"actor\",\n        \"defaultEnabled\": true,\n        \"enabled\": false\n      }\n    ],\n    \"alwaysSent\": [\n      {\n        \"label\": \"Email confirmation links\",\n        \"description\": \"Sent when you add or change an address.\"\n      }\n    ]\n  }\n}"
            },
            {
              "name": "Unknown key",
              "originalRequest": {
                "method": "PUT",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/notifications/preferences"
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"message\": \"Unknown notification: render_finished.\",\n  \"code\": \"UNKNOWN_NOTIFICATION\",\n  \"errors\": {\n    \"render_finished\": \"Not a notification you can change.\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Send a test alert",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/notifications/test",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "notifications",
                "test"
              ]
            },
            "description": "Sends one sample alert to the signed-in account’s confirmed address, and nowhere else. Use it to prove delivery before you need it — an address that bounces or a filter that files us under Promotions is not something to discover the night a workflow breaks.\n\nLimited to 5 an hour per account. Returns 422 when the account has no confirmed address, and 502 when the mail provider refused the message."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/notifications/test"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Test email sent\",\n  \"data\": {\n    \"sentTo\": \"priya@company.com\"\n  }\n}"
            }
          ]
        },
        {
          "name": "List alerts we have sent",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{authToken}}",
                "description": "Dashboard session token, not an API key."
              },
              {
                "key": "X-Pixbix-Org",
                "value": "{{orgId}}",
                "description": "Which workspace the history is about."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/notifications/history?limit=25",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "notifications",
                "history"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "25",
                  "description": "1–100. Defaults to 25.",
                  "disabled": true
                }
              ]
            },
            "description": "What was actually sent for this workspace, newest first — the answer to \"did you tell us?\".\n\nIt also explains a gap. Alerts are throttled per situation (one per failing endpoint per day, one per failing API route per hour, one per workflow failure streak), so a row here at 02:00 is why the 03:00 failure produced no second email. Requires `org:read`."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/notifications/history"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"notifications\": [\n      {\n        \"id\": \"ntl_4c1a9f\",\n        \"key\": \"workflow_failed\",\n        \"label\": \"A workflow stopped working\",\n        \"email\": \"priya@company.com\",\n        \"subject\": \"“Daily product posters” failed\",\n        \"sentAt\": \"2026-08-16T02:00:11.204Z\"\n      }\n    ]\n  }\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Public (site)",
      "description": "Unauthenticated endpoints behind the marketing site. No API key; rate limited by IP.",
      "item": [
        {
          "name": "Submit a contact enquiry",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Priya Sharma\",\n  \"email\": \"priya@example.com\",\n  \"company\": \"Acme Retail\",\n  \"topic\": \"sales\",\n  \"message\": \"We generate about 4,000 product creatives a month and want to automate it.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/contact",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "contact"
              ]
            },
            "description": "Delivers the enquiry to the support inbox with the sender on Reply-To.\n\nNo authentication. Limited to 5 requests per hour per IP; a sixth returns 429.\n\nBody parameters:\n- `name` (string) — required — At least 2 characters.\n- `email` (string) — required — Where the reply goes.\n- `company` (string) — Optional.\n- `topic` (string) — required — sales, support, billing, partnership, privacy or other.\n- `message` (string) — required — At least 10 characters.\n- `website` (string) — Honeypot. Leave empty; a filled value is silently discarded."
          },
          "response": [
            {
              "name": "Success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/contact"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"Thanks — we will be in touch.\",\n  \"data\": {\n    \"received\": true\n  }\n}"
            }
          ]
        }
      ]
    }
  ]
}