{
  "info": {
    "name": "Mencoro API v1",
    "_postman_id": "3ccb0703-66ae-42a0-9d4e-3487d990042f",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "description": "Every Mencoro API v1 operation, grouped by resource in the contract's own order. Generated from openapi.json by `npm run api:artifacts`. Set the api_key collection variable to a key from https://tool.mencoro.com/me/api-keys and organization_id to one of the organizations that key reaches, then run any request. Optional query parameters ship disabled: tick the ones you need. Writes carry an Idempotency-Key that Postman regenerates per send, which is what you want while exploring and not what you want when retrying a lost response."
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://api.mencoro.com/api/v1"
    },
    {
      "key": "api_key",
      "value": ""
    },
    {
      "key": "organization_id",
      "value": ""
    }
  ],
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{api_key}}",
        "type": "string"
      }
    ]
  },
  "item": [
    {
      "name": "Tracked Queries",
      "item": [
        {
          "name": "Search a project's tracked queries",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries?limit=20&offset=0&search=&status=active&engines=chatgpt&countries=&sortBy=queryText&sortOrder=desc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A value above the maximum is rejected, never clamped.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "",
                  "description": "Free-text search over the keyword.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "active",
                  "disabled": true
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated.",
                  "disabled": true
                },
                {
                  "key": "countries",
                  "value": "",
                  "description": "ISO-3166 alpha-2 codes or English names. Must be configured on the project.",
                  "disabled": true
                },
                {
                  "key": "sortBy",
                  "value": "queryText",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. One row per tracked query — a single keyword on a single engine in a single country — carrying the metrics of its most recent completed check. Filter by status, engine, country and a free-text search over the keyword, and sort by any of the returned metrics. Positions (lastSerpPosition, lastMentionPosition, lastLinkPosition, lastShoppingPosition) are 1-based ranks, so LOWER is better; lastShareOfVoice and lastPositivityIndex are percentages from 0 to 100, where HIGHER is better. Every nullable field means \"not known yet\" rather than zero: a null position is a query with no data for that surface, a null lastPositivityIndex is a check with no mentions to score, and a null lastCheckedAt is a query that has never been checked — none of them is a score of zero. This listing reads a projection refreshed by background subscribers, not the write model, so a tracked query created or changed moments ago may not appear here yet or may still show its previous settings. It catches up on its own; nothing is lost. If you need to read back what you just wrote, the creation response carries the new ids and the single tracked-query operation reads the write model directly. total counts the tracked queries the filters match, not the rows on this page. A limit above the maximum is rejected, never clamped, and a filter this endpoint does not support is rejected rather than ignored.Send `Accept: text/csv` to receive the same page as a CSV download instead of JSON: same filters, same authorization, same page window and the same maximum of 100 rows — it is this page in another format, not a bulk export, so a whole collection is still read by paging. The CSV carries no `total`, because a table whose every row is a record has nowhere to put one; read it from the JSON representation of the same request. A list-valued field is joined into one cell with `; ` as a display projection — parse the JSON if you need the structure. Cells beginning with `=`, `+`, `-` or `@` are prefixed with an apostrophe so a spreadsheet treats them as text rather than running them as formulas."
          }
        },
        {
          "name": "Create tracked queries",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Creates the cross product of `queryTexts` x `engines` x `countries`: three texts, two engines and two countries create twelve tracked queries, not three. At most 100 combinations per call. The organization must be active and the project must not be archived. Partial success: the response lists, per combination, either the id it was created under or why nothing was created for it, and the status code never reports item-level outcomes. A combination the project already tracks is NOT created again and NOT re-identified — it appears under `failed` with `tracked_query_already_exists`, keeps the id it already had, and has any `queryClusterIds` in this request merged into it. Query text is normalised before it is compared and stored (lower-cased, whitespace collapsed, leading list markers stripped), so two texts differing only in those respects are one tracked query: the first of them owns the outcome and every later one appears under `failed` with `duplicate_combination_in_request`, naming the entry it repeats. `nPasses` applies to AI engines only: `google_serp` and `google_shopping` rows are always created with one pass, whatever is sent. Google AI Mode is unavailable in a few countries and those combinations are reported under `failed` rather than created. Creating a tracked query does not run a check: the first check happens on the normal schedule for the `checkFrequency` chosen. The Idempotency-Key header is required; a retry with the same key and the same body returns this same answer without creating anything again.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per operation. Replaying it returns the first answer instead of creating anything again."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"queryTexts\": [\n    \"\"\n  ],\n  \"engines\": [\n    \"chatgpt\"\n  ],\n  \"countries\": [\n    \"\"\n  ],\n  \"locale\": \"\",\n  \"checkFrequency\": \"daily\",\n  \"nPasses\": 1,\n  \"queryClusterIds\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get a tracked query",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: viewer. The configuration of one tracked query: the text sent to the engine, the engine, locale and country it is asked in, the clusters it belongs to, and how often it is checked. A lastCheckedAt of null means no check has completed yet — it is not a check that found nothing. A tracked query belonging to another project answers 404, the same answer an unknown id gets, so the API never confirms that an inaccessible tracked query exists. Rank positions, share of voice and sentiment are not part of this response: they belong to a date window and are served by the analytics endpoints."
          }
        },
        {
          "name": "Change how often a tracked query is checked",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId/check-frequency",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId",
                "check-frequency"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Sets how often one tracked query is checked while it is active. The organization must be active and the project must not be archived. Setting the frequency it already has is accepted and changes nothing. This does NOT run a check, does not backfill history, and does not reschedule a check already in flight: the new cadence applies from the next time the query is considered. A paused query keeps the setting but is not checked until it is resumed. A tracked query belonging to another project answers 404, the same answer an unknown id gets. The Idempotency-Key header is required; a retry with the same key and the same body returns this same answer without applying anything again.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per operation. Replaying it returns the first answer instead of applying anything again."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"checkFrequency\": \"daily\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Add a tracked query to clusters",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId/clusters",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId",
                "clusters"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Adds the tracked query to every cluster named in \"queryClusterIds\" and answers with the query in its new state. Membership is a set: a cluster the query already belongs to is skipped, not reported as an error, and the response still lists it. Clusters the query belongs to and this call does not name are left alone — this adds, it does not replace the membership. Every cluster must belong to the project in the path; one that does not is rejected with its field named, and nothing is written. The \"Idempotency-Key\" header is required, and a repeat of the same key and body returns the recorded answer without adding anything again.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per operation, so a lost response can be retried without repeating the write."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"queryClusterIds\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Remove a tracked query from clusters",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId/clusters",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId",
                "clusters"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Removes the tracked query from every cluster named in \"queryClusterIds\" and answers with the query in its new state. A cluster the query does not belong to is skipped, not reported as an error. Neither the clusters nor the tracked query are deleted: only the membership between them. Every cluster must belong to the project in the path. NOTE: this DELETE requires a request body — some HTTP client libraries and proxies strip bodies from DELETE, and a stripped body is refused with a validation error rather than interpreted as \"remove all clusters\". The \"Idempotency-Key\" header is required, and a repeat of the same key and body returns the recorded answer without removing anything again.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per operation, so a lost response can be retried without repeating the write."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"queryClusterIds\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List stored mention matches of a tracked query",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId/mention-matches?dateFrom=2026-01-01&dateTo=2026-01-01&limit=20&offset=0&sortOrder=desc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId",
                "mention-matches"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive UTC day; defaults to the retention floor.",
                  "disabled": true
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive UTC day.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Text mentions across own brand and tracked or untracked competitors. Citation-only rows are excluded before pagination. Read mentionRelation to distinguish own brand from untracked competitors; a null competitorId alone does not classify the mention. Newest first by detection time, with an id tie-break. Dates cover whole UTC days. Only the retained 16-month window is readable, including when dateFrom is omitted. Unknown filters are rejected. The total counts all matching rows before pagination. Send Accept: text/csv for the same bounded page and filters as CSV, with formula-safe cells and no total."
          }
        },
        {
          "name": "Change how many passes a tracked query runs per check",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId/passes",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId",
                "passes"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Sets how many times one tracked query is asked per check. AI engines are not deterministic, so several passes are averaged; `google_serp` and `google_shopping` run a single pass and refuse any value above one with 409 `n_passes_not_supported_for_engine`. Setting the value back to 1 is always allowed. The organization must be active and the project must not be archived. Setting the value it already has is accepted and changes nothing. This does NOT run a check and does not change history already captured: it applies from the next check. More passes cost proportionally more of the plan's check budget. A tracked query belonging to another project answers 404, the same answer an unknown id gets. The Idempotency-Key header is required; a retry with the same key and the same body returns this same answer without applying anything again.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per operation. Replaying it returns the first answer instead of applying anything again."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"nPasses\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Pause a tracked query",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId/pause",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId",
                "pause"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Stops the scheduler from checking this tracked query; it keeps its configuration, its clusters and every result already collected, and nothing is deleted. Pausing a query that is already paused succeeds and answers the same body — this is a PUT asserting a state, not a transition, so it is safe to repeat. It does NOT cancel a check that is already running: a check in flight when the pause lands still completes and still consumes the budget unit it reserved. The request takes no body, and any field sent is rejected. Requires an Idempotency-Key header.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeating a request with the same key answers with the first attempt's result instead of pausing again."
              }
            ]
          }
        },
        {
          "name": "Report a problem with a captured AI answer",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId/responses/:aiResponseId/report",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId",
                "responses",
                ":aiResponseId",
                "report"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                },
                {
                  "key": "aiResponseId",
                  "value": "",
                  "description": "A capture id from the AI responses listing. Must belong to the tracked query in the path."
                }
              ]
            },
            "description": "Minimum role: viewer — deliberately lower than the other tracked-query writes, because a report changes nothing a viewer cannot already read. The key still needs the \"write\" capability. The report is forwarded to the team that reviews the scrape run behind the capture; it does not change the capture, the tracked query, or any metric derived from them, and nothing in this API will show the report afterwards. A 200 means the report was accepted for review, not that anything was corrected, and there is no identifier to poll. A capture that belongs to another tracked query or project answers 404, the same answer an unknown id gets. A capture that cannot be routed back to its scrape run answers 409 \"ai_check_session_unavailable\", and it cannot be reported. That code covers two moments, which behave differently for your key: when the capture carries no scrape run at all the refusal is decided BEFORE anything is sent, so the idempotency key is left unused and the same key answers the same way however often it is presented; when the review system itself rejects the run — its own retention having elapsed — the refusal comes after the forward was attempted, so the key is spent like any outcome we cannot confirm. You can tell them apart without guessing: retry the same key, and a reply of \"operation_outcome_uncertain\" means the refusal came from the review system. Delivery is at-most-once: the \"Idempotency-Key\" header is required and a repeat of the same key and body returns the recorded answer, but a delivery whose outcome is unknown refuses replay on that key rather than risk filing the report twice.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per report, so a lost response can be retried without filing the report twice."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"type\": \"missed_mention\",\n  \"comment\": \"\",\n  \"missedBrandNames\": [\n    \"\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Resume a tracked query",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId/resume",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId",
                "resume"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Puts a paused tracked query back under the scheduler. Resuming a query that is already active succeeds and answers the same body — this is a PUT asserting a state, not a transition. It does NOT run a check: the query is checked when it next falls due under its own checkFrequency, and results collected while it was paused are unaffected. Nothing is back-filled for the time it spent paused. The request takes no body, and any field sent is rejected. Requires an Idempotency-Key header.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeating a request with the same key answers with the first attempt's result instead of resuming again."
              }
            ]
          }
        },
        {
          "name": "List stored serp matches of a tracked query",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId/serp-matches?dateFrom=2026-01-01&dateTo=2026-01-01&limit=20&offset=0&sortOrder=desc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId",
                "serp-matches"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive UTC day; defaults to the retention floor.",
                  "disabled": true
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive UTC day.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Stored organic-search matches with the competitor attribution and position recorded at detection time. A null competitorId identifies the own-brand match. These are historical matches, not a reclassification using the current brand profile. Newest first by detection time, with an id tie-break. Dates cover whole UTC days. Only the retained 16-month window is readable, including when dateFrom is omitted. Unknown filters are rejected. The total counts all matching rows before pagination. Send Accept: text/csv for the same bounded page and filters as CSV, with formula-safe cells and no total."
          }
        },
        {
          "name": "Change how often several tracked queries are checked",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/batch/check-frequency",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                "batch",
                "check-frequency"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Sets the same check frequency on every tracked query named in `ids`, at most 100 distinct ids per call. The organization must be active and the project must not be archived. Partial success: an id that is not a tracked query of this project — unknown, malformed, already deleted, or belonging to somewhere else — is reported under `failed` with `tracked_query_not_found` while the rest are changed, and the call still answers 200. Nothing is rolled back because an item failed. Setting the frequency a query already has is accepted and changes nothing. This does NOT run a check and does not reschedule one already in flight. It never changes more than the ids it is given: there is no \"change everything matching a filter\" mode. The Idempotency-Key header is required; a retry with the same key and the same body returns this same answer without applying anything again.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per operation. Replaying it returns the first answer instead of applying anything again."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ],\n  \"checkFrequency\": \"daily\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Change how many passes several tracked queries run per check",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/batch/passes",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                "batch",
                "passes"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Sets the same passes-per-check on every tracked query named in `ids`, at most 100 distinct ids per call. The organization must be active and the project must not be archived. Partial success, and two distinct reasons appear under `failed`: an id that is not a tracked query of this project answers `tracked_query_not_found`, and a `google_serp` or `google_shopping` query asked for more than one pass answers `n_passes_not_supported_for_engine` — those engines run a single pass. Both leave the rest of the batch changed and the call still answers 200. Setting the value back to 1 is always allowed, so a batch that lowers sampling never fails on engine grounds. This does NOT run a check and does not change history already captured. More passes cost proportionally more of the plan's check budget. It never changes more than the ids it is given: there is no \"change everything matching a filter\" mode. The Idempotency-Key header is required; a retry with the same key and the same body returns this same answer without applying anything again.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per operation. Replaying it returns the first answer instead of applying anything again."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ],\n  \"nPasses\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Pause several tracked queries",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/batch/pause",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                "batch",
                "pause"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Pauses up to 100 tracked queries of one project, each independently. Always answers 200 when the batch itself was processed: read `failed` to find out which items were not paused, never the status code. Items are never rolled back because a later one failed. An id that is already paused is reported as successful — pausing asserts a state, not a transition. An id that is not a tracked query of this project is reported as failed with `tracked_query_not_found`, exactly as an id that does not exist at all, and nothing is written for it. Pausing does not cancel a check that is already running. Requires an Idempotency-Key header; a retry must repeat the same ids in the same order to be recognised as a retry rather than refused as a reused key.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeating a request with the same key and the same ids answers with the first attempt's result."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Resume several tracked queries",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/batch/resume",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                "batch",
                "resume"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Puts up to 100 paused tracked queries of one project back under the scheduler, each independently. Always answers 200 when the batch itself was processed: read `failed` to find out which items were not resumed, never the status code. Items are never rolled back because a later one failed. An id that is already active is reported as successful — resuming asserts a state, not a transition. An id that is not a tracked query of this project is reported as failed with `tracked_query_not_found`, exactly as an id that does not exist at all, and nothing is written for it. No check is run by this operation and nothing is back-filled for the time the queries spent paused. Requires an Idempotency-Key header; a retry must repeat the same ids in the same order to be recognised as a retry rather than refused as a reused key.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeating a request with the same key and the same ids answers with the first attempt's result."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete tracked queries",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/bulk-delete",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                "bulk-delete"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Permanently deletes the tracked queries named in `ids`, at most 100 distinct ids per call. The organization must be active and the project must not be archived. Deletion is hard and cannot be undone: the tracked query is removed along with its captured answers, matches, search pages and rank history, and any check already in flight for it is cancelled. Those cascades run in the background, so a 200 means the tracked queries were deleted, not that every derived record has finished being purged. Partial success: an id that is not a tracked query of this project — unknown, malformed, already deleted, or belonging to somewhere else — is reported under `failed` with `tracked_query_not_found` while the rest are deleted. It never deletes more than the ids it is given: there is no \"delete everything matching a filter\" mode. The Idempotency-Key header is required; a retry with the same key and the same body returns this same answer without deleting anything again.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per operation. Replaying it returns the first answer instead of deleting anything again."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Add many tracked queries to clusters",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/bulk/clusters",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                "bulk",
                "clusters"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Adds every tracked query named in \"ids\" to every cluster named in \"queryClusterIds\". At most 100 distinct tracked queries per call; duplicates in \"ids\" are collapsed. Partial success: the answer is 200 with a per-item \"successful\" and \"failed\" list even when some items failed, nothing is rolled back, and an id that is not a tracked query of this project is reported as a failed item rather than dropped. Clusters not named are left alone — this adds, it does not replace membership. Every cluster must belong to the project in the path; one that does not is rejected with its field named and nothing is written at all. The \"Idempotency-Key\" header is required, and a repeat of the same key and body returns the recorded answer without running the batch again.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per operation, so a lost response can be retried without repeating the batch."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ],\n  \"queryClusterIds\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Remove many tracked queries from clusters",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/bulk/clusters",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                "bulk",
                "clusters"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Removes every tracked query named in \"ids\" from every cluster named in \"queryClusterIds\". At most 100 distinct tracked queries per call; duplicates in \"ids\" are collapsed. Partial success: the answer is 200 with a per-item \"successful\" and \"failed\" list even when some items failed, nothing is rolled back, and an id that is not a tracked query of this project is reported as a failed item rather than dropped. Neither the clusters nor the tracked queries are deleted: only the membership between them. Every cluster must belong to the project in the path; one that does not is rejected with its field named and nothing is written at all. NOTE: this DELETE requires a request body — some HTTP client libraries and proxies strip bodies from DELETE, and a stripped body is refused with a validation error rather than interpreted as \"remove everything\". The \"Idempotency-Key\" header is required, and a repeat of the same key and body returns the recorded answer without running the batch again.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A client-chosen key, unique per operation, so a lost response can be retried without repeating the batch."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ],\n  \"queryClusterIds\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Check several tracked queries now",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/check",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                "check"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Asks for a fresh check of up to 100 named tracked queries straight away, ignoring how recently each was last checked. Answers 200 with per-item results and NO job id: a check that is already in flight for a query is reused rather than started again, so there is no id this operation could hand back that is guaranteed to exist. Follow progress on the tracked query itself — its lastCheckedAt advances when the check completes. A successful item means the check was accepted for submission with budget available for it at that moment; it does not mean the check has run. A checked query costs one budget unit per pass (nPasses), and items that do not fit the remaining budget are reported as failed with `check_budget_forecast_exhausted`, or `subscription_not_found` when the organization has no entitled subscription — they are never reported as successful. That budget figure is a forecast for this batch, and a pessimistic one: a tracked query already being checked is joined to the check in flight and costs nothing, but it is still debited here, so an item refused this way may have fitted. Resubmit it in a later batch rather than treating the refusal as a statement about your subscription. A paused query is reported as failed with `tracked_query_already_paused`: paused queries are never checked. An id that is not a tracked query of this project is reported as failed with `tracked_query_not_found`, exactly as an id that does not exist. Requires an Idempotency-Key header; a retry must repeat the same ids in the same order.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeating a request with the same key and the same ids answers with the first attempt's result instead of submitting again."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Check every eligible tracked query of a project now",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/check-all",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                "check-all"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Submits a fresh check for every eligible tracked query of the project, ignoring how recently each was last checked, least-recently-checked first, and at most 1000 tracked queries per call. Eligible is narrower than active: a paused query is skipped, and so is one whose check is already pending or running for the same engine. A query whose check is awaiting a retry is included and, WHEN THE ENGINE HAS NOT CHANGED SINCE, costs nothing extra, because its first submission already paid for it; if the engine did change, the pending run is replaced and the replacement is paid for. What is left is trimmed to what the organization's remaining check budget can pay for — a check costs one budget unit per pass. While a subscription is cancelled but still inside its paid grace window nothing is submitted at all and `submitted` is 0; name the queries explicitly through the check operation to run them in that window. CALLING AGAIN DOES NOT CONTINUE WHERE THIS CALL STOPPED: submissions are handed to a worker, and a tracked query stops being selected only once that worker has started its check, so a second call made before the queue drains selects and submits the same tracked queries again. That is safe while the first check is still running — the duplicate is collapsed, and nothing is checked or charged twice — but a check that has already finished is run again and charged again, because this operation ignores staleness by design. To cover a project with more than 1000 eligible tracked queries, wait for the submitted wave to be picked up — each tracked query's lastCheckedAt advances when its check completes — and call again then. The response lists the tracked queries submitted and carries NO job id: a check already in flight is reused rather than started again, so no id could be guaranteed to exist. Submission is accepted, not completed: a listed tracked query can still lose the last budget units to another caller before the worker reaches it. The request takes no body, and any field sent is rejected. Requires an Idempotency-Key header.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeating a request with the same key answers with the first attempt's result instead of submitting a second wave."
              }
            ]
          }
        },
        {
          "name": "Count a project's tracked queries and price checking them",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/count?status=active",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                "count"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "active",
                  "description": "Restrict both numbers to one status. Lower-case; an unknown value is rejected, not ignored. Omitted, every status is counted and priced.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Two numbers about one project: how many tracked queries it holds, and what force-checking that same set would cost. Omit `status` to count every tracked query whatever its status; send `active` or `paused` to count and price only those. `checkCost` is a PRICED DRY RUN expressed in check budget units — one unit per pass, the same unit the plan allowance is counted in, so it is directly comparable with `checksAvailable` from the entitlements operation — and it is the sum of each matched tracked query's configured passes. Asking reserves nothing, debits nothing and starts no check. It is deliberately NOT a forecast of what the check-all operation will consume: that operation skips paused queries, skips a query whose check is already pending or running, re-runs a check awaiting retry without charging for it again, and stops at whatever budget is left — none of which is subtracted here. Count with `status=active` for the figure closest to a full check-all. Both numbers are read from the write model, so a tracked query created moments ago is already in them; that is why they can be AHEAD of the `total` returned by the tracked-queries listing, which counts a search projection, and ahead of the keyword listings, which read projections refreshed in the background. Summing the unfiltered count over every project of an organization, archived projects included, reproduces countOrganizationTrackedQueries, which counts through the same counter in the same store. An archived project still answers, because this is a read, but no check can be submitted there while it stays archived, so its cost is hypothetical. For a month of scheduled rounds across the whole organization instead of one round over one project, read getOrganizationProjectedMonthlyChecks. No monetary amount is published or implied: the cost of a check is published only in budget units."
          }
        }
      ]
    },
    {
      "name": "Analytics",
      "item": [
        {
          "name": "Map everyday wording to a metric and the operation that serves it",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/metric-glossary",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "metric-glossary"
              ]
            },
            "description": "Static reference, no project data. Each entry gives a metric, the everyday words people use for it, its unit and range, whether higher or lower is better, the operation that returns it, and example questions. Useful when turning a vague or non-technical request into the right call."
          }
        },
        {
          "name": "Snapshot rank-health board across an organization active projects",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/overview",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "overview"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: viewer. One row per active project — share of voice, mention rate, average mention position, positivity index and tracked-query count — ordered by share of voice, plus an organization-level aggregate of the same metrics. Archived projects are excluded. This is a current-state snapshot and takes no date window; for a date-ranged comparison call the per-project operations (getProjectMetrics, getProjectTimeSeries) for each projectId returned here. Every metric is nullable, and a null means the project has no rank data yet — not a score of zero. Higher is better for shareOfVoice, mentionRate and positivityIndex; avgMentionPosition is a 1-based rank, so LOWER is better."
          }
        },
        {
          "name": "Filter values a project is configured for",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/available-filters",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "available-filters"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Call this first: it is where every other analytics operation sends you for the valid engines, countries and keyword clusters of a project, and the values it returns are the exact strings the engines, countries and queryClusterIds parameters accept — anything else is rejected as a 400. Engines are engine codes, countries are ISO-3166 alpha-2 codes, and clusters are {id, name} objects whose id goes in queryClusterIds. Takes no date window: it describes how the project is configured right now, so a value is listed as soon as a tracked query uses it, even when no response has been captured for it yet. An empty list therefore means nothing is configured for it, not that no data was collected. Competitor ids are not part of this response."
          }
        },
        {
          "name": "Domains and pages the AI answers cited",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/cited-sources?dateFrom=2026-01-01&dateTo=2026-01-01&engines=chatgpt&groupBy=domain&limit=20&offset=0",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "cited-sources"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d.",
                  "disabled": false
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated. Only the AI engines carry citations.",
                  "disabled": true
                },
                {
                  "key": "groupBy",
                  "value": "domain",
                  "description": "Grain of the roll-up: \"domain\" by host, \"page\" by exact URL.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A larger value is rejected, never silently reduced.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Number of sources to skip.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. The sources the answer engines drew on across a project's AI answers over a date window, ranked by how often they were cited. Per source: citationCount, the total number of citations; distinctResponseCount and distinctQueryCount, how many captured answers and tracked queries it appeared in; avgPosition, its average 1-based rank inside the answers' citation lists, where LOWER is better. A null avgPosition means no citation in the window carried a position, not a rank of zero; a null domain or sampleTitle means the citation never carried one. `total` counts the distinct sources matching the window, before paging. The list is UNFILTERED by ownership: the brand's, competitors' and third-party sources sit in the same ranking. Only the AI answer engines (chatgpt, perplexity, google_ai_overview, google_ai_mode) produce citations, so filtering by google_serp or google_shopping is accepted and returns nothing. Citations still behind an answer engine's redirect (a google.com/goto link, whose URL names the engine rather than the source) are excluded, so a source cited only through such links is absent from this list rather than counted as zero."
          }
        },
        {
          "name": "Head-to-head record of the brand against each tracked competitor",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/competitors/co-occurrence?dateFrom=2026-01-01&dateTo=2026-01-01&engines=chatgpt&countries=&competitorId=00000000-0000-0000-0000-000000000000",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "competitors",
                "co-occurrence"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d.",
                  "disabled": false
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated. SERP and Shopping carry no AI answer text, so they contribute no co-occurrence.",
                  "disabled": true
                },
                {
                  "key": "countries",
                  "value": "",
                  "description": "ISO-3166 alpha-2 codes or English names. Must be configured on the project.",
                  "disabled": true
                },
                {
                  "key": "competitorId",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "description": "Restricts the answer to a single tracked competitor. Omit it for every tracked competitor. The available-filters endpoint lists the valid ids.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Restricted to the AI answers where the brand and a competitor are BOTH mentioned, one row per tracked competitor: sharedResponseCount is how many such answers there are, and brandWins / competitorWins / ties split them by who holds the better (lower) best mention position. winRate is the percentage 0-100 of those answers the brand wins; avgOwnPosition and avgCompetitorPosition are the average best mention position each side held, 1-based, so LOWER is better. exampleQueryText and exampleAiResponseId point at one representative shared answer. Every nullable field means \"not known yet\" rather than zero: a null winRate or average position is the absence of a shared answer in the window, not a record of losing. Tracked competitors only."
          }
        },
        {
          "name": "Coverage and staleness of a project tracked queries",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/coverage",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "coverage"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. A current-state snapshot answering \"what is stale or not being tracked\": total is every tracked query on the project, active and paused split it by status, neverChecked counts the active queries that have never run, and overdue counts the active queries whose last check is older than their own check-frequency interval (daily, weekly or monthly). neverChecked and overdue are disjoint — a query that has never run is never also counted as overdue — and both ignore paused queries, which are not expected to be checked at all. sample lists up to 20 of the overdue queries, most stale first; it is a sample of overdue only, so it never contains a never-checked query and is empty when overdue is 0. Takes no date window: every count describes the project as it stands right now."
          }
        },
        {
          "name": "List a project's keywords with their windowed metrics",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/keyword-listings?dateFrom=2026-01-01&dateTo=2026-01-01&engines=chatgpt&countries=&queryClusterIds=00000000-0000-0000-0000-000000000000&includeUngroupedQueries=false&status=active&checkFrequencies=daily&nPasses=1&search=&sortBy=keyword&sortOrder=asc&limit=20&offset=0",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "keyword-listings"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d. The trend compares against the equally long window ending the day before dateFrom.",
                  "disabled": false
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated. Narrows which variants count towards each row.",
                  "disabled": true
                },
                {
                  "key": "countries",
                  "value": "",
                  "description": "ISO-3166 alpha-2 codes or English names. Must be configured on the project.",
                  "disabled": true
                },
                {
                  "key": "queryClusterIds",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "description": "Restrict to keywords with a variant in these clusters. Each must belong to the project.",
                  "disabled": true
                },
                {
                  "key": "includeUngroupedQueries",
                  "value": "false",
                  "description": "Sent alone, restricts the listing to keywords whose variants belong to no cluster; sent with queryClusterIds, adds them to that selection.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "active",
                  "description": "Restrict to variants with this status. A keyword whose variants disagree still reports statusSummary \"mixed\".",
                  "disabled": true
                },
                {
                  "key": "checkFrequencies",
                  "value": "daily",
                  "description": "Repeatable, or comma-separated.",
                  "disabled": true
                },
                {
                  "key": "nPasses",
                  "value": "1",
                  "description": "Repeatable, or comma-separated. Restrict to variants configured with these pass counts.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "",
                  "description": "Case-insensitive substring match on the keyword text.",
                  "disabled": true
                },
                {
                  "key": "sortBy",
                  "value": "keyword",
                  "description": "Named after the field it orders by. Rows with no value for the chosen key sort last in either direction. An unknown key is rejected, not replaced by the default.",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "asc",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A larger value is rejected, never silently reduced.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Number of matching keywords to skip before the page starts.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. One row per distinct keyword text of the project — every tracked query asking that text, on any engine in any country, collapsed into a single row whose variantIds name the tracked queries behind it. Each row carries the metrics of the requested window and a signed trend against the window of equal length immediately before it, where POSITIVE ALWAYS MEANS BETTER whichever direction the metric itself runs. Positions are 1-based and lower is better; rates, positivityIndex and shareOfVoice are 0-100 and higher is better; mentionPositionStability is a day-to-day spread, so lower is steadier. A null metric means nothing was captured for that keyword in the window — it is not a zero. `total` counts the KEYWORDS matching the filters, not the rows on this page and not tracked queries; `totalVariantCount` counts the tracked queries behind the keywords matching every filter EXCEPT `search` — with a text search applied it still counts the project's variants, so do not size a force-check budget from it on a searched page. NEITHER IS THE PROJECT'S TRACKED-QUERY COUNT: the count operation reads the write model, while this listing reads projections refreshed in the background from it, so the numbers legitimately differ while those projections catch up. A project whose cached tracked-query count has not been refreshed yet answers an empty page with total 0 — the same answer as a project with no tracked queries at all — so treat an unexpected empty page right after creating queries as \"not projected yet\", not as \"no data\". dataDirtySince is non-null while a recalculation is pending, meaning the metrics predate the latest configuration change. Filters narrow which VARIANTS count towards a row, so engines, countries, statuses, checkFrequencies and nPassesValues each report what the filters selected rather than everything the keyword has. A filter or sort key this endpoint cannot honour is rejected by name, never ignored, and a limit above the maximum is rejected rather than quietly reduced. This operation answers JSON only: a keyword row carries a nested mentionTypeCounts object, so it is not part of the CSV family."
          }
        },
        {
          "name": "Sample of the raw AI mention texts of a project",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/mentions?dateFrom=2026-01-01&dateTo=2026-01-01&engines=chatgpt&countries=&sentiment=positive&mentionType=recommendation&competitorId=00000000-0000-0000-0000-000000000000&sortBy=recent&limit=20&offset=0",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "mentions"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d.",
                  "disabled": false
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated. Non-AI engines contribute no mentions.",
                  "disabled": true
                },
                {
                  "key": "countries",
                  "value": "",
                  "description": "ISO-3166 alpha-2 codes or English names. Must be configured on the project.",
                  "disabled": true
                },
                {
                  "key": "sentiment",
                  "value": "positive",
                  "description": "Restrict to one sentiment label. Omit for every sentiment.",
                  "disabled": true
                },
                {
                  "key": "mentionType",
                  "value": "recommendation",
                  "description": "Restrict to one mention type. Omit for every type.",
                  "disabled": true
                },
                {
                  "key": "competitorId",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "description": "UUID of a single competitor, as listed by the available-filters endpoint (competitors[].id). Omitted, the page is restricted to mentions storing no competitor id, which is own-brand and untracked-competitor mentions together — see the operation description and read mentionRelation to tell them apart.",
                  "disabled": true
                },
                {
                  "key": "sortBy",
                  "value": "recent",
                  "description": "recent: newest first. negative: negative sentiment first, then neutral, then positive, newest first inside each. engine and country: grouped alphabetically, newest first inside each group. An unknown value is rejected, not replaced by the default.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A larger value is rejected, never silently reduced.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Number of matching mentions to skip before the page starts.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. A paginated page of the individual mention texts behind the aggregate numbers, for qualitative review and for checking sentiment labels by eye. Each sample carries the mention text, the engine and country it was seen in, the tracked query that produced it, its sentiment and mention type, and mentionPosition — a 1-based rank inside the answer where LOWER is better, always present. `total` counts every mention matching the filters, not the size of the page returned. Only mentions inside the answer text are returned: a citation of the brand URL is not a mention here, and only AI answer engines produce mentions, so restricting engines to google_serp or google_shopping alone returns an empty page rather than an error. Two fields carry a \"not known\" rather than a zero: country is null and queryText is empty when the tracked query behind the mention has since been deleted, and competitorId is null when the mention row stores no competitor id — which is NOT an assertion that the mention is about your own brand, since an untracked competitor also stores none. Read mentionRelation instead: `own` and `tracked-competitor` are what the scraper resolved to a configured entity, `untracked-competitor` is a rival the project does not track, and null means the row predates the field. brandName carries the mentioned brand, and is the only way to name an untracked competitor, which has no competitor id to resolve one from. Omitting the competitorId filter returns exactly the rows with no competitor id stored — that is, own-brand AND untracked-competitor mentions together, not every competitor; pass a competitor UUID to restrict the page to that competitor, and use mentionRelation to separate the rest. For the aggregate positive/neutral/negative split use the sentiment endpoint, and for weighted mention-type counts the mention mix endpoint."
          }
        },
        {
          "name": "Composition of a project brand mentions in AI answers",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/mentions/mix?dateFrom=2026-01-01&dateTo=2026-01-01&engines=chatgpt&countries=",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "mentions",
                "mix"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d.",
                  "disabled": false
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated. Non-AI engines contribute no mentions.",
                  "disabled": true
                },
                {
                  "key": "countries",
                  "value": "",
                  "description": "ISO-3166 alpha-2 codes or English names. Must be configured on the project.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Counts of the project brand's own text mentions in AI answers over a date window, grouped three ways: byType (recommendation, comparison, listing, example, reference), byTone (positive, neutral, negative) and byQualifier (direct, conditional — a conditional mention is one the answer hedged with a condition). These are the inputs behind the Share of Voice weighted score. Every bucket is always present and is a plain count, never null: a zero means no mention of that kind was found in the window. The three groupings count the same mentions, so each one sums to the same total. Only the project brand is counted, never a competitor, and only mentions inside the answer text — a citation of the brand's URL is not a mention here. Only AI answer engines produce mentions, so restricting engines to google_serp or google_shopping alone returns all zeros. For the positive/neutral/negative split per engine and per competitor use the sentiment endpoint."
          }
        },
        {
          "name": "Headline visibility metrics of a project",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/metrics?dateFrom=2026-01-01&dateTo=2026-01-01&engines=chatgpt&countries=&queryClusterIds=00000000-0000-0000-0000-000000000000&includeUngroupedQueries=false",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "metrics"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d.",
                  "disabled": false
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated.",
                  "disabled": true
                },
                {
                  "key": "countries",
                  "value": "",
                  "description": "ISO-3166 alpha-2 codes or English names. Must be configured on the project.",
                  "disabled": true
                },
                {
                  "key": "queryClusterIds",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "description": "Restrict to these keyword clusters. Each must belong to this project.",
                  "disabled": true
                },
                {
                  "key": "includeUngroupedQueries",
                  "value": "false",
                  "description": "Only meaningful together with queryClusterIds: also counts the tracked queries that belong to no cluster.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. The project overview over a date window: share of voice (own and per competitor), mention / SERP / shopping rates, average and best positions, position stability, the sentiment split and the position-distribution buckets. Positions are 1-based, so a LOWER number is better; rates, the positivity index and share of voice are percentages from 0 to 100, where HIGHER is better. Every `trend*` field is the signed change against the immediately preceding window of the same length: negative means an improved position, positive means an improved rate or score. A null metric means \"not known yet\", never zero: a scalar is null when the window holds no checks at all, and a `trend*` field is null when there is no earlier window to compare against. The counters (`mentionCount`, `*TrackedQueryCount`, `*QueriesWithResult`, `sentiment*` and `mentionTypeCounts`) are genuine zeros instead, so an empty window reads as zero counts with null rates. `dataDirtySince` is non-null while a recalculation is pending, meaning the figures may still move for dates from then on."
          }
        },
        {
          "name": "Rank-tracking metrics per keyword cluster",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/metrics/clusters?dateFrom=2026-01-01&dateTo=2026-01-01&engines=chatgpt&countries=&queryClusterIds=00000000-0000-0000-0000-000000000000&includeUngroupedQueries=false",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "metrics",
                "clusters"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d.",
                  "disabled": false
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated.",
                  "disabled": true
                },
                {
                  "key": "countries",
                  "value": "",
                  "description": "ISO-3166 alpha-2 codes or English names. Must be configured on the project.",
                  "disabled": true
                },
                {
                  "key": "queryClusterIds",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "description": "Restrict to these clusters. Each must belong to the project.",
                  "disabled": true
                },
                {
                  "key": "includeUngroupedQueries",
                  "value": "false",
                  "description": "Sent alone, returns only the ungrouped bucket rather than adding it to every cluster.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. One row per keyword cluster over a date window, with its tracked query and keyword counts, average positions, rates, share of voice and sentiment split. A row whose clusterId is null is the ungrouped bucket: the tracked queries belonging to no cluster. Position metrics are 1-based and LOWER is better; rates, positivityIndex and shareOfVoice are 0-100 percentages where higher is better. A null metric means nothing was captured for that cluster in the window — it is not a zero, and averaging or charting it as one would misstate the period. dataDirtySince is non-null while a recalculation is pending, meaning the numbers predate the latest configuration change."
          }
        },
        {
          "name": "Tracked queries ranked by how much a metric moved",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/metrics/movers?dateFrom=2026-01-01&dateTo=2026-01-01&engines=chatgpt&countries=&sortBy=trend_share_of_voice&sortOrder=desc&limit=20&offset=0",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "metrics",
                "movers"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window. The comparison window is the equally long stretch immediately before it.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d.",
                  "disabled": false
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated.",
                  "disabled": true
                },
                {
                  "key": "countries",
                  "value": "",
                  "description": "ISO-3166 alpha-2 codes or English names. Must be configured on the project.",
                  "disabled": true
                },
                {
                  "key": "sortBy",
                  "value": "trend_share_of_voice",
                  "description": "Which trend delta ranks the rows. Positions improve as they fall, so a positive delta is always an improvement whichever key you pick. An unknown value is rejected, not replaced by the default.",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "description": "desc for the top gainers, asc for the top losers.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A value above the maximum is rejected, never clamped.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. One row per tracked query — a single engine plus country — carrying its current metric envelope and the signed change against the immediately preceding window of equal length: a 7-day window is compared with the 7 days before it. Every trend delta is signed so that POSITIVE means improved, including the position trends, where the underlying avgSerpPosition / avgShoppingPosition / avgMentionPosition / avgLinkPosition are 1-based ranks and therefore LOWER is better. shareOfVoice and positivityIndex are percentages from 0 to 100, where HIGHER is better. Sorting applies to the trend keys only: sortOrder=desc gives the top gainers, asc the top losers. Every nullable field means \"not known yet\" rather than zero — a null position or shareOfVoice is a query with no data in the window, and a null positivityIndex or trend is a period with no mentions to score, neither of which is a record of losing ground. total counts the tracked queries the filters match, not the rows on this page. dataDirtySince is a date from which the rank data is being recomputed, or null when nothing is pending; while it is non-null the deltas may still move."
          }
        },
        {
          "name": "The constants behind the Share of Voice score",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/metrics/share-of-voice-formula",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "metrics",
                "share-of-voice-formula"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Static reference data, the same for every project: the weights and multipliers that turn individual brand mentions into a Share of Voice score. Each mention is worth `mentionTypeWeights[type] * sentimentMultipliers[tone] * (conditional ? conditionalMultiplier : directMultiplier)`, and a competitor's Share of Voice is its share of the summed weights of every brand in the window, as a percentage. mentionTypeWeights is keyed by mention type (recommendation, comparison, listing, example, reference) and sentimentMultipliers by tone (positive, neutral, negative); a negative mention is discounted, not discarded, because it still evidences presence. A conditional mention is one the answer hedged with a condition (\"if you need X\"). Use this to explain a score, not to recompute one: the counts it applies to come from the mention mix endpoint. Every value is always present and is never null."
          }
        },
        {
          "name": "Sentiment breakdown of a project brand mentions",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/sentiment?dateFrom=2026-01-01&dateTo=2026-01-01&engines=chatgpt&countries=&queryClusterIds=00000000-0000-0000-0000-000000000000&includeUngroupedQueries=false",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "sentiment"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d.",
                  "disabled": false
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated.",
                  "disabled": true
                },
                {
                  "key": "countries",
                  "value": "",
                  "description": "ISO-3166 alpha-2 codes or English names. Must be configured on the project.",
                  "disabled": true
                },
                {
                  "key": "queryClusterIds",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "disabled": true
                },
                {
                  "key": "includeUngroupedQueries",
                  "value": "false",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Positive, neutral and negative split of the brand mentions in AI answers over a date window, per engine and per competitor. A null positivityIndex means no mentions were found in the window, which is not the same as a score of zero."
          }
        },
        {
          "name": "Rank-tracking metrics of a project over time",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/timeseries?dateFrom=2026-01-01&dateTo=2026-01-01&granularity=daily&engines=chatgpt&countries=&queryClusterIds=00000000-0000-0000-0000-000000000000&includeUngroupedQueries=false&competitorIds=00000000-0000-0000-0000-000000000000",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "timeseries"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d.",
                  "disabled": false
                },
                {
                  "key": "granularity",
                  "value": "daily",
                  "description": "Bucket size of each point.",
                  "disabled": true
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Repeatable, or comma-separated.",
                  "disabled": true
                },
                {
                  "key": "countries",
                  "value": "",
                  "description": "ISO-3166 alpha-2 codes or English names. Must be configured on the project.",
                  "disabled": true
                },
                {
                  "key": "queryClusterIds",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "disabled": true
                },
                {
                  "key": "includeUngroupedQueries",
                  "value": "false",
                  "description": "On its own this NARROWS the series to tracked queries that belong to no cluster; combined with queryClusterIds it widens those clusters to also cover them.",
                  "disabled": true
                },
                {
                  "key": "competitorIds",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "description": "Repeatable, or comma-separated. Each id adds one series under the competitors map of every point.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. One point per bucket over the date window, each carrying the brand metrics and one same-shaped entry per requested competitor. Rank metrics (serp, shopping, mention, link) are 1-based averages where LOWER is better; positivity (0-100), shareOfVoice (0-100), mentionRate (0-100) and serpRate (0-100) are scores where higher is better. Every metric is nullable, and a null means no data was collected for that bucket, which is not the same as a value of zero. A project with no tracked queries returns an empty points list. Prefer weekly or monthly granularity over a long window to keep the response compact."
          }
        },
        {
          "name": "Rank-tracking time series of a single tracked query",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/tracked-queries/:trackedQueryId/timeseries?dateFrom=2026-01-01&dateTo=2026-01-01&granularity=daily&competitorIds=00000000-0000-0000-0000-000000000000",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "tracked-queries",
                ":trackedQueryId",
                "timeseries"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive start of the window, Y-m-d. Must fall inside the data retention window.",
                  "disabled": false
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive end of the window, Y-m-d.",
                  "disabled": false
                },
                {
                  "key": "granularity",
                  "value": "daily",
                  "description": "Bucket size. Prefer weekly or monthly for long windows.",
                  "disabled": true
                },
                {
                  "key": "competitorIds",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "description": "Competitors to add as extra series, repeatable or comma-separated. Valid ids come from the available-filters endpoint.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "trackedQueryId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: viewer. One point per bucket over the date window, each carrying the brand metrics and one same-shaped entry per requested competitor. Positions (serp, shopping, mention, link) are 1-based, so a LOWER number is better; positivity, shareOfVoice, mentionRate and serpRate are percentages from 0 to 100, where higher is better. Every metric is nullable, and a null means nothing was captured for that entity in that bucket — it is not a zero: a null shareOfVoice means no measurement, a shareOfVoice of 0 means measured and never mentioned. The tracked query fixes its own engine and country, so no engine or country filter is accepted. A non-null dataDirtySince is a timestamp warning that tracked queries were deleted from the project and historical buckets may still include their contributions until the nightly refresh rebuilds them."
          }
        }
      ]
    },
    {
      "name": "Organizations",
      "item": [
        {
          "name": "List accessible organizations",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations?limit=20&offset=0&search=&status=active&sortBy=createdAt&sortOrder=desc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "active",
                  "disabled": true
                },
                {
                  "key": "sortBy",
                  "value": "createdAt",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "disabled": true
                }
              ]
            },
            "description": "Returns the organizations the key's owner is an active member of, narrowed to the key's scope. A key scoped to all organizations also sees organizations joined after it was created.Send `Accept: text/csv` to receive the same page as a CSV download instead of JSON: same filters, same authorization, same page window and the same maximum of 100 rows — it is this page in another format, not a bulk export, so a whole collection is still read by paging. The CSV carries no `total`, because a table whose every row is a record has nowhere to put one; read it from the JSON representation of the same request. Cells beginning with `=`, `+`, `-` or `@` are prefixed with an apostrophe so a spreadsheet treats them as text rather than running them as formulas."
          }
        },
        {
          "name": "Create an organization",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations"
              ]
            },
            "description": "Requires a key scoped to all organizations and the \"organization:manage\" capability: a key limited to named organizations cannot widen its own reach by creating one. Preview it first and send the confirmation in X-Mencoro-Confirmation together with an Idempotency-Key.",
            "header": [
              {
                "key": "X-Mencoro-Confirmation",
                "value": ""
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get an organization",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: viewer. An organization outside the key's scope, or one the caller is not an active member of, answers 404 - the API never confirms that an inaccessible organization exists."
          }
        },
        {
          "name": "Update an organization profile",
          "request": {
            "method": "PATCH",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: owner. A partial update: omit a field to leave it alone, send it as null to clear it. Preview it first; the confirmation is bound to the current values, so an edit made by somebody else in between invalidates it rather than being silently overwritten. The organization image is deliberately NOT writable here, although the Mencoro app accepts one in the equivalent call: a published JSON API is the wrong place to carry a 10MB base64 blob in a body that also has to be fingerprinted for idempotency and digested for the confirmation. The image stays readable as `imageUrl`; changing it is done in the app.",
            "header": [
              {
                "key": "X-Mencoro-Confirmation",
                "value": ""
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\",\n  \"description\": \"\",\n  \"contactEmail\": \"someone@example.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Archive an organization",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/archive",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "archive"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: owner. Archiving also archives the active projects of the organization, cancels its pending invitations and cancels its subscription at the end of the current billing period. Those effects are applied by background subscribers, so a 200 means the organization was archived, not that every effect has finished. Preview it first to see exactly what will be touched.",
            "header": [
              {
                "key": "X-Mencoro-Confirmation",
                "value": "",
                "description": "The `confirmation.token` returned by POST /api/v1/organization-operation-previews for this exact action, organization and body. Single use, valid for five minutes, and bound to the effects the preview declared: if the organization gained a project or lost an invitation in between, it is refused with 409 rather than doing more than was agreed. Without it the call answers 428."
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeat it to retry a lost response. The replay is answered from the record without claiming the confirmation again, so a retry is never refused for presenting a confirmation the first attempt already spent."
              }
            ]
          }
        },
        {
          "name": "Get an organization's plan allowance and consumption",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/entitlements",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "entitlements"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: owner. Returns what the current plan allows, how much of it has been consumed and when the allowance next resets, taken from the most recent subscription contract whether it is running or cancelled. An organization that has never subscribed answers `status: \"none\"` with every budget and consumption field null — null means \"no plan on file, so not known\", which is deliberately distinct from a budget or a consumption of zero. Commercial and provider details (prices, Stripe identifiers, internal tier codes) are not part of this contract."
          }
        },
        {
          "name": "Membership, project and invitation counts for an organization",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/membership-stats",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "membership-stats"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: owner. Pre-computed counts of the organization's members, projects and outstanding invitations, read from a materialized view that is refreshed periodically — `computedAt` says when the snapshot was taken, so a member added since the last refresh is not counted yet. Every count is always an integer and a zero means zero; an organization whose row has not been computed yet answers 404 with code `organization_membership_stats_not_found`, never a body of zeros, so \"none\" and \"not known yet\" are never confused. Counts cover the whole organization, active and inactive alike: `totalMembersCount` includes suspended members, and `totalProjectsCount` includes archived projects. `pendingInvitationsCount` counts only invitations that are still pending and not yet expired."
          }
        },
        {
          "name": "Restore an archived organization",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/restore",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "restore"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: owner. Restoring reinstates the projects that were archived as part of archiving this organization - projects archived on their own stay archived - and aborts a pending subscription cancellation. Invitations cancelled by the archive are not reinstated. Preview it first to see which effects are reversible.",
            "header": [
              {
                "key": "X-Mencoro-Confirmation",
                "value": ""
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ]
          }
        },
        {
          "name": "Headline counts for an organization",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/stats",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "stats"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: viewer. Active members, projects (total and active) and pending invitations, in one call. Every value is a count and is always known: 0 means the organization really has none of that thing, and no field is ever null. `members.total` counts ACTIVE memberships only, so a suspended member is not included; `projects.total` counts every project including archived ones, and `projects.active` the non-archived subset. This is a current-state snapshot and takes no date window."
          }
        },
        {
          "name": "Project a month of check consumption from the current tracking configuration",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/usage/projected-monthly-checks",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "usage",
                "projected-monthly-checks"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: viewer. What the organization's current configuration would consume in a month, in check budget units — the same unit `checkBudget` and `checksAvailable` are counted in by the entitlements operation — so the two are directly comparable when sizing a plan. The arithmetic is published so it can be reproduced rather than trusted: for each ACTIVE tracked query, runs per month (daily 30, weekly 4, monthly 1) multiplied by its nPasses, summed. A weekly query bills 4, not 4.345: a month is modelled as 30 days and 4 weeks, a planning convention rather than a calendar. Take `checkFrequency` and `nPasses` from the tracked-query listing and the arithmetic will match — though the two sides are read from different places and settle at different times: this figure is computed in the Postgres write model and served from a cache that background subscribers invalidate, while that listing reads an Elasticsearch projection. Immediately after a write the two can disagree; neither is wrong, they are catching up. PAUSED queries are excluded from both figures; archived PROJECTS are not — archiving a project does not pause its tracked queries, so they still count here, exactly as they do in countOrganizationTrackedQueries. `activeTrackedQueryCount` describes the same population as that count, minus the paused queries — but do not compute the difference to learn how many are paused: THIS OPERATION IS CACHED and that one is read live, so the two can disagree while the cache is invalidated in the background. This is a projection of the configuration, not a forecast of what will actually be spent: it does not look at the remaining plan budget, does not know which checks will be skipped or retried, and reserves and debits nothing. It is also not the `checkCost` of countTrackedQueries, which prices one round over one project rather than a month over the organization. Both values are never null: 0 means nothing is scheduled. This operation takes no query parameters — the population is fixed, and a parameter it cannot honour is rejected with 400 rather than silently ignored."
          }
        },
        {
          "name": "Count the tracked queries an organization has configured",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/usage/tracked-queries",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "usage",
                "tracked-queries"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: viewer. How many tracked queries the organization has configured, counted live in PostgreSQL — the write model — over EVERY project it owns, archived projects included, and over every tracked query in them, active and paused alike. It is deliberately a DIFFERENT number from `aggregate.totalTrackedQueries` in getOrganizationOverview: that one sums a per-project copy held in the Elasticsearch read model and covers ACTIVE projects only, so it excludes archived projects and can lag behind a change that already shows here. Expect the two to disagree and do not treat either as wrong. This figure does reconcile exactly with countTrackedQueries, which counts one project through the same counter in the same store: sum its unfiltered `count` over every project, archived included, and you get this number. For the ACTIVE subset and what it will consume, call getOrganizationProjectedMonthlyChecks, which ranges over the same projects. Do NOT derive the paused count by subtracting one from the other: this number is read live from Postgres while that one is served from a cache invalidated by background subscribers, so the two can disagree while that invalidation catches up and the difference is then a count of nothing. For how many projects this ranges over, read `projects.total` and `projects.active` from getOrganizationStats. The value is never null: 0 means none are configured. This operation takes no query parameters — the population is fixed, and a parameter it cannot honour is rejected with 400 rather than silently ignored."
          }
        }
      ]
    },
    {
      "name": "Billing",
      "item": [
        {
          "name": "Get the subscription of an organization",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/subscription",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "subscription"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: viewer. The most recent subscription contract of the organization, whatever its state: tier, billing interval, check budget and consumption, and the cancellation and grace dates. An organization that has never subscribed answers 200 with \"status\": \"none\" and every other field null - a null is \"not applicable\", never a stand-in for a zero budget or zero consumption. Stripe identifiers, prices and payment methods are not part of this API."
          }
        }
      ]
    },
    {
      "name": "Captures",
      "item": [
        {
          "name": "List captured AI answers",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/ai-responses?dateFrom=2026-01-01&dateTo=2026-01-01&trackedQueryId=00000000-0000-0000-0000-000000000000&engines=chatgpt&limit=20&offset=0&sortOrder=desc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "ai-responses"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive lower bound, widened to 00:00:00 UTC of the named day. Must be inside the retention window.",
                  "disabled": true
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive upper bound, widened to 23:59:59 UTC of the named day.",
                  "disabled": true
                },
                {
                  "key": "trackedQueryId",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "description": "Only captures of this tracked query. Must belong to the project in the path.",
                  "disabled": true
                },
                {
                  "key": "engines",
                  "value": "chatgpt",
                  "description": "Only captures from these AI engines. Repeat the parameter or pass a comma-separated list. An empty filter means every engine.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A larger value is rejected, never silently reduced.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Number of captures to skip before the page starts.",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "description": "Direction of the capture-time ordering. An unknown value is rejected, not replaced by the default.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Every AI answer captured for a project, newest first, one page at a time. This is the evidence under the analytics operations: the full answer text and the sources the engine cited, exactly as captured. Read `unresolved` on a citation before attributing its domain — a true value means the URL is still a redirector and the domain belongs to that redirector, not to the publisher; count the citation, do not credit the site. `passIndex` and `passCount` describe multi-pass sampling: rows sharing a `trackedQueryId` and `capturedAt` are passes of one run, not duplicates, so aggregating across them without dividing by `passCount` double-counts that run. `responseText` is the whole answer, so a page of 100 is a large response — lower `limit` rather than paging blind. Captures are kept for 16 months and purged after that; a `dateFrom` before the window is rejected rather than answered with an empty page. `total` counts every capture the filter matches, not the size of this page."
          }
        },
        {
          "name": "List captured search-results pages",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/search-snapshots?dateFrom=2026-01-01&dateTo=2026-01-01&trackedQueryId=00000000-0000-0000-0000-000000000000&limit=20&offset=0&sortOrder=desc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "search-snapshots"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive lower bound, widened to 00:00:00 UTC of the named day. Must be inside the retention window.",
                  "disabled": true
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive upper bound, widened to 23:59:59 UTC of the named day.",
                  "disabled": true
                },
                {
                  "key": "trackedQueryId",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "description": "Only captures of this tracked query. Must belong to the project in the path.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A larger value is rejected, never silently reduced.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Number of captures to skip before the page starts.",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "description": "Direction of the capture-time ordering. An unknown value is rejected, not replaced by the default.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Every organic search page captured for a project, newest first, one page at a time. This is what the rank figures in the analytics operations were computed from: the ranked results as they stood at `capturedAt`, not as they stand now. Read `unresolved` on a result before attributing its domain — a true value means the URL is still a redirector and the domain belongs to that redirector; count the result, do not credit the site. `rating` and `ratingVotes` come from a rich-result star rating when Google showed one and are absent far more often than present; their absence says nothing about the page. There is no `engines` filter because every capture here comes from Google SERP. Captures are kept for 16 months and purged after that; a `dateFrom` before the window is rejected rather than answered with an empty page. `total` counts every capture the filter matches, not the size of this page."
          }
        },
        {
          "name": "List captured shopping-results pages",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/shopping-snapshots?dateFrom=2026-01-01&dateTo=2026-01-01&trackedQueryId=00000000-0000-0000-0000-000000000000&limit=20&offset=0&sortOrder=desc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "shopping-snapshots"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "2026-01-01",
                  "description": "Inclusive lower bound, widened to 00:00:00 UTC of the named day. A date before the retention floor is refused rather than answered with an empty page: shopping captures are aged out by the raw-data purge like every other capture.",
                  "disabled": true
                },
                {
                  "key": "dateTo",
                  "value": "2026-01-01",
                  "description": "Inclusive upper bound, widened to 23:59:59 UTC of the named day.",
                  "disabled": true
                },
                {
                  "key": "trackedQueryId",
                  "value": "00000000-0000-0000-0000-000000000000",
                  "description": "Only captures of this tracked query. Must belong to the project in the path.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A larger value is rejected, never silently reduced.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Number of captures to skip before the page starts.",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "description": "Direction of the capture-time ordering. An unknown value is rejected, not replaced by the default.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Every shopping page captured for a project, newest first, one page at a time: the ranked offers as they stood at `capturedAt`. `price` and `currency` are what the seller showed at that moment — no tax normalisation, no shipping, no conversion — and two offers in one snapshot may carry different currencies, so convert before comparing. `productId` is the marketplace's own identifier for the listing, stable enough to follow one offer across snapshots, and it is not a Mencoro id. Offers turn over far faster than organic results, so two snapshots days apart routinely share none; that is the marketplace behaving normally, not a gap in the capture. Unlike organic results, a shopping offer carries no `unresolved` flag: the capture pipeline does not record one for this surface. Shopping captures are trimmed by the same raw-data purge as AI answers and search pages, so the same retention floor applies: a `dateFrom` before it is refused rather than answered with an empty page. `total` counts every capture the filter matches, not the size of this page."
          }
        }
      ]
    },
    {
      "name": "Clusters",
      "item": [
        {
          "name": "Create a keyword cluster",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/clusters",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "clusters"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Creates one empty keyword cluster in the project. The name is trimmed and lower-cased before it is stored, and it must be unique within the project: a name that already exists is refused with 409 and nothing is merged into the existing cluster. The cluster starts with no tracked queries in it — this operation does not assign anything to it. The organization must not be archived; an archived project IS refused with 409, stricter than the Mencoro app, which lets cluster writes into one. Requires the write capability and an Idempotency-Key header.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A token you choose per operation, 8 to 255 printable ASCII characters with no spaces; a UUID is the obvious choice. Repeat it to retry a lost response without creating a second cluster. The key is scoped to your API key, and it is bound to the method, the path and the body of the first attempt: repeating it with anything else changed is refused with 409 rather than replayed."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get one of a project's keyword clusters",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/clusters/:clusterId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "clusters",
                ":clusterId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": "",
                  "description": "Must belong to the organization in the path."
                },
                {
                  "key": "clusterId",
                  "value": "",
                  "description": "Must be a cluster of the project in the path."
                }
              ]
            },
            "description": "Minimum role: viewer. Returns a single keyword cluster of the project, the same projection the cluster listing returns for each of its rows. A cluster belonging to another project answers 404, the same answer an unknown id and a malformed one get, so the API never confirms that a cluster the caller cannot reach exists."
          }
        },
        {
          "name": "Rename a keyword cluster",
          "request": {
            "method": "PATCH",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/clusters/:clusterId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "clusters",
                ":clusterId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "clusterId",
                  "value": "",
                  "description": "Must be a cluster of the project in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Changes the name of one keyword cluster and nothing else: the tracked queries assigned to it are untouched, and its id does not change, so nothing a client stored breaks. The new name is trimmed and lower-cased and must be unique within the project; a collision is refused with 409. Renaming to the name the cluster already has is accepted and is a no-op. The organization must not be archived; an archived project IS refused with 409, stricter than the Mencoro app, which lets cluster writes into one. Requires the write capability and an Idempotency-Key header.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeat it to retry a lost response without renaming twice."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete a keyword cluster",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/clusters/:clusterId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "clusters",
                ":clusterId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "clusterId",
                  "value": "",
                  "description": "Must be a cluster of the project in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Deletes one keyword cluster. The tracked queries that were in it are NOT deleted: they are unassigned from this cluster and keep every other cluster they belong to. Analytics filtered by this cluster id return nothing afterwards, including for dates before the deletion, because that filter reads current membership. This cannot be undone — recreating a cluster with the same name produces a new id and an empty cluster. The organization must not be archived, and neither must the project: an archived project IS refused with 409, stricter than the Mencoro app, which lets cluster writes into one. Requires the write capability and an Idempotency-Key header.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeat it to retry a lost response; a fresh key answers 404 once the cluster is gone."
              }
            ]
          }
        },
        {
          "name": "Create several keyword clusters at once",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/clusters/batch",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "clusters",
                "batch"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Creates up to 100 empty keyword clusters in one call. Names are trimmed and lower-cased and repeated names in the body are collapsed before anything is written. Partial success: the answer is always 200 and reports every name under `successful` or under `failed` — a name already taken in the project fails with query_cluster_name_already_exists and is NOT resolved to the existing cluster, while the other names are still created. Nothing is rolled back because one name failed. The clusters are created empty; no tracked query is assigned to them. The organization must not be archived; an archived project IS refused with 409, stricter than the Mencoro app, which lets cluster writes into one. Requires the write capability and an Idempotency-Key header.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeat it to retry a lost response without creating the batch twice."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"names\": [\n    \"\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Start a keyword clustering job",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/clusters/jobs",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "clusters",
                "jobs"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager. Asks the clustering service to propose keyword clusters for up to 500 tracked queries of the project, and answers 202 with the job to poll — the work runs in the background and a 202 says it was accepted, never that it succeeded. THE JOB WRITES NOTHING: it produces a proposal, and nothing changes until it is applied through the apply operation, which is a separate call. Duplicate query texts within the selection are sent once. Requires an entitled subscription (402 otherwise) and the organization must be active and the project not archived. Rate limited to 10 starts per minute per organization, shared with the same operation in the web application. Requires the write capability and an Idempotency-Key header; repeating the key returns the first job rather than starting a second.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeat it to retry a lost response without starting a second job."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"trackedQueryIds\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ],\n  \"mode\": \"fill_gaps\",\n  \"restrictToExistingClusters\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Apply the result of a clustering job",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/clusters/jobs/:jobId/apply",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "clusters",
                "jobs",
                ":jobId",
                "apply"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                },
                {
                  "key": "jobId",
                  "value": "",
                  "description": "Must be a clustering job started for this organization and project."
                }
              ]
            },
            "description": "Minimum role: manager. Writes a completed clustering job onto the tracked queries it was computed for: it creates the clusters the job proposed that the project does not have yet, then assigns each tracked query according to the merge mode the job was started with — fill_gaps leaves already grouped queries alone, add_on_top only adds, full_regroup replaces a query's clusters with the proposed set and therefore REMOVES clusters that are not in it — but only for a tracked query the job actually returned an assignment for. A tracked query the job was started over and produced no assignment for is listed under `unassigned` and left exactly as it was, under every mode; it is not treated as \"belongs to no cluster\" and is never stripped. That matters most with restrictToExistingClusters, where the model is expected to return nothing for queries that fit no existing cluster. The body must be empty: the assignments, the target tracked queries and the mode all come from the job, so this call cannot apply something the job did not produce. Synchronous and partially successful — always 200, with each tracked query under `successful` or `failed`, and nothing rolled back because one failed. A job that is not completed, or one that produced no assignments, is refused with 409. Requires the write capability and an Idempotency-Key header; repeating the key returns the first answer rather than applying twice.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeat it to retry a lost response without applying the job twice."
              }
            ]
          }
        }
      ]
    },
    {
      "name": "Projects",
      "item": [
        {
          "name": "List an organization's projects",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects?limit=20&offset=0&search=&status=active&sortBy=createdAt&sortOrder=desc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A larger value is rejected, never silently reduced.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Rows to skip before the page starts. Page by advancing it in steps of `limit` until it reaches `total`.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "active",
                  "disabled": true
                },
                {
                  "key": "sortBy",
                  "value": "createdAt",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: viewer. Metrics come from the same read model the application uses, so the figures match what the product shows. A null metric means \"not known yet\", never zero.Send `Accept: text/csv` to receive the same page as a CSV download instead of JSON: same filters, same authorization, same page window and the same maximum of 100 rows — it is this page in another format, not a bulk export, so a whole collection is still read by paging. The CSV carries no `total`, because a table whose every row is a record has nowhere to put one; read it from the JSON representation of the same request. A list-valued field is joined into one cell with `; ` as a display projection — parse the JSON if you need the structure. Cells beginning with `=`, `+`, `-` or `@` are prefixed with an apostrophe so a spreadsheet treats them as text rather than running them as formulas."
          }
        },
        {
          "name": "Create a project and the brand monitoring profile its checks run against",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: manager, in an active organization. Requires the \"write\" capability. Creates the project, the brand monitoring profile holding the domains and brand names to watch, and one competitor per entry of `competitors`, in a single call. The project id is minted by the server; a caller-supplied id is rejected as an unknown field. What is NOT done: no tracked queries are created, no check is run and no scraping is scheduled — a new project has nothing collected against it until tracked queries are added. Each website entry may be a full URL or a bare domain: a URL is reduced to its host with any leading \"www.\" removed, so \"https://www.acme.com/pricing\" is stored as \"acme.com\". Duplicate domains and duplicate brand names are collapsed, exactly as the stored value objects do. Send an Idempotency-Key: a retry with the same key and the same body returns this same project instead of creating a second one.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\",\n  \"websiteDomains\": [\n    \"\"\n  ],\n  \"brandNames\": [\n    \"\"\n  ],\n  \"competitors\": [\n    {\n      \"name\": \"\",\n      \"websiteDomains\": [\n        \"\"\n      ],\n      \"brandNames\": [\n        \"\"\n      ]\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get a project and its brand monitoring configuration",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. Returns the project together with the domains and brand names it is monitored for and the competitors it is measured against. A project that exists but belongs to another organization answers 404, never 403. A project that has been created but not yet configured for brand monitoring reports empty `websiteDomains`, `brandNames` and `competitors`. Headline metrics are not part of this response: use `listProjects` for the per-project figures, or `getProjectMetrics` for a window."
          }
        },
        {
          "name": "Rename a project",
          "request": {
            "method": "PATCH",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager, in an active organization. Requires the \"write\" capability. `name` is the only writable field and it is required. What is NOT done: the monitored domains and brand names are not touched (use updateProjectBrandProfile) and competitors are not touched, added or removed (use the competitor endpoints). Sending `websiteDomains`, `brandNames` or `competitors` here is refused with the field named, never applied in part and never ignored. Renaming a project changes nothing about the data already collected against it. A project that belongs to another organization answers 404, never 403. An archived project answers 409: restore it first — unless the call is a retry carrying the key of a rename that already succeeded, which is answered from the record whatever the project's state is now. Send an Idempotency-Key; a retry with the same key and body returns the recorded answer.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Archive a project",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/archive",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "archive"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager, in an active organization. Requires the \"write\" capability. Archiving stops a project from being modified: its name, brand profile and competitors are refused with 409 until it is restored. What is NOT done: nothing is deleted. Tracked queries, captured responses, mentions and every metric already collected stay exactly as they are, and restoreProject brings the project back with all of it. Archiving is recorded as done by the caller, not by an organization cascade, so restoring the organization later will not restore this project — restore it explicitly. This endpoint takes no body, and one carrying fields is refused. Archiving an already archived project answers 409, unless the call is a retry carrying the key that archived it. Send an Idempotency-Key; a retry with the same key returns the recorded answer.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ]
          }
        },
        {
          "name": "Get a project's brand monitoring profile",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/brand-profile",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "brand-profile"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": "",
                  "description": "Must belong to the organization in the path."
                }
              ]
            },
            "description": "Minimum role: viewer. The brand identity every check of this project is matched against: the tracked brand terms, the website domains, and the generated description of what the brand does. A null description means it has not been generated yet — the generator runs asynchronously after the names or domains change — which is not the same as an empty one. An empty brandNames or websiteDomains array means the profile exists and names nothing; a project whose profile has not been created at all answers 404."
          }
        },
        {
          "name": "Replace a project's brand monitoring profile",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/brand-profile",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "brand-profile"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": "",
                  "description": "Must belong to the organization in the path."
                }
              ]
            },
            "description": "Minimum role: manager, on an active organization and a project that is not archived. Replaces the brand terms and website domains every check of this project is matched against: the lists sent become the lists stored, so a term left out is removed. Both lists are required and neither may be empty — a project must keep at least one brand name and one domain to match anything. A domain may be sent as a full URL or as a bare host; a URL is reduced to its host and a leading \"www.\" is dropped, which is the form the profile is read back in. Duplicates, including two URLs that reduce to the same host, are collapsed. This operation does NOT touch the project name or its competitors, which are separate resources, and it does not regenerate the brand description: that runs asynchronously afterwards, so the description in the response is the one stored at the time of the write. An Idempotency-Key header is required.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique per attempt. A retry carrying the same key and the same body is answered from the record instead of running again."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"websiteDomains\": [\n    \"\"\n  ],\n  \"brandNames\": [\n    \"\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List the keyword clusters of a project",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/clusters?limit=20&offset=0&sortOrder=asc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "clusters"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A larger value is rejected, never silently reduced.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Number of clusters to skip before the page starts.",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "asc",
                  "description": "Direction of the name ordering. An unknown value is rejected, not replaced by the default.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. The keyword clusters configured on a project, one page at a time. Each cluster id is exactly what the analytics operations accept in their queryClusterIds filter — pass the id, never the name. Names are unique within a project and are stored lower-cased, so the listing is ordered by name with no ties and paging over it neither repeats nor skips a cluster. `total` counts every cluster in the project, not the size of the page returned. A cluster carries no metrics of its own and no membership count: for the tracked queries inside a cluster, filter the tracked-query operations by its id. An empty list means the project has no clusters configured, which is not an error and is not a statement about whether any data has been collected.Send `Accept: text/csv` to receive the same page as a CSV download instead of JSON: same filters, same authorization, same page window and the same maximum of 100 rows — it is this page in another format, not a bulk export, so a whole collection is still read by paging. The CSV carries no `total`, because a table whose every row is a record has nowhere to put one; read it from the JSON representation of the same request. A list-valued field is joined into one cell with `; ` as a display projection — parse the JSON if you need the structure. Cells beginning with `=`, `+`, `-` or `@` are prefixed with an apostrophe so a spreadsheet treats them as text rather than running them as formulas."
          }
        },
        {
          "name": "List the competitors tracked by a project",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/competitors?limit=20&offset=0&sortOrder=asc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "competitors"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size. A larger value is rejected, never silently reduced.",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Number of competitors to skip before the page starts.",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "asc",
                  "description": "Direction of the id ordering. An unknown value is rejected, not replaced by the default.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: viewer. The competitors configured on the project, one page at a time, with the website domains and brand names each one is matched against. `total` counts every competitor of the project, not the size of this page, so a project with more than `limit` competitors needs `offset` to read them all. Ordering is by id, which for a UUID v7 is roughly creation order, and `sortOrder` chooses its direction; there is no other sort key and no text search, and sending `sortBy` or `search` is rejected rather than ignored. A project whose brand monitoring profile has not been created yet answers 200 with an empty collection, which means \"nothing configured yet\" rather than \"no competitors found\". Internal fields the pipeline writes — the auto-generated brand description used by the mention classifier, and the internal brand monitoring profile id — are not part of this contract.Send `Accept: text/csv` to receive the same page as a CSV download instead of JSON: same filters, same authorization, same page window and the same maximum of 100 rows — it is this page in another format, not a bulk export, so a whole collection is still read by paging. The CSV carries no `total`, because a table whose every row is a record has nowhere to put one; read it from the JSON representation of the same request. A list-valued field is joined into one cell with `; ` as a display projection — parse the JSON if you need the structure. Cells beginning with `=`, `+`, `-` or `@` are prefixed with an apostrophe so a spreadsheet treats them as text rather than running them as formulas."
          }
        },
        {
          "name": "Add a competitor to a project",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/competitors",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "competitors"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": "",
                  "description": "Must belong to the organization in the path."
                }
              ]
            },
            "description": "Minimum role: manager, on an active organization and a project that is not archived. Creates one competitor with the domains and brand names its mentions are matched against. Both lists are required and neither may be empty: a competitor that matches on nothing would never be found in an answer. A domain may be sent as a full URL or as a bare host; a URL is reduced to its host and a leading \"www.\" is dropped. Duplicates are collapsed. The id is assigned by the server and cannot be chosen, and neither the auto-generated brand description nor the internal brand monitoring profile id can be set — sending either is refused as an unknown field. Adding a competitor does NOT re-match the answers already captured: it applies to checks from here on. A project whose brand monitoring profile has not been created yet answers 404. An Idempotency-Key header is required.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique per attempt. Without it a lost response cannot be retried without risking a second competitor."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\",\n  \"websiteDomains\": [\n    \"\"\n  ],\n  \"brandNames\": [\n    \"\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get one of a project's competitors",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/competitors/:competitorId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "competitors",
                ":competitorId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": "",
                  "description": "Must belong to the organization in the path."
                },
                {
                  "key": "competitorId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: viewer. Returns a single competitor of the project, the same projection the competitor listing returns for each of its rows. A competitor belonging to another project answers 404, the same answer an unknown id and a malformed one get, so the API never confirms that a competitor the caller cannot reach exists. A project whose brand monitoring profile has not been created yet has no competitors at all and answers 404 for any competitor id."
          }
        },
        {
          "name": "Replace a competitor",
          "request": {
            "method": "PUT",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/competitors/:competitorId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "competitors",
                ":competitorId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": "",
                  "description": "Must belong to the organization in the path."
                },
                {
                  "key": "competitorId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: manager, on an active organization and a project that is not archived. Replaces the competitor's name, domains and brand names: the lists sent become the lists stored, so a term left out is removed. Both lists are required and neither may be empty — a competitor that matches on nothing would never be found in an answer. A domain may be sent as a full URL or as a bare host; a URL is reduced to its host and a leading \"www.\" is dropped. Duplicates are collapsed. The auto-generated brand description cannot be set, and sending it is refused as an unknown field. Changing the matching rules does NOT re-match the answers already captured: it applies to checks from here on. A competitor belonging to another project answers 404, the same answer an unknown id gets. An Idempotency-Key header is required.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique per attempt. A retry carrying the same key and the same body is answered from the record instead of running again."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\",\n  \"websiteDomains\": [\n    \"\"\n  ],\n  \"brandNames\": [\n    \"\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Remove a competitor from a project",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/competitors/:competitorId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "competitors",
                ":competitorId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": "",
                  "description": "Must belong to the organization in the path."
                },
                {
                  "key": "competitorId",
                  "value": "",
                  "description": "Must belong to the project in the path."
                }
              ]
            },
            "description": "Minimum role: manager, on an active organization and a project that is not archived. Removes the competitor and, asynchronously, every stored mention, search result and shopping result attributed to it, in AI answers and search captures already taken. This is permanent and it changes historical analytics: share of voice and competitor co-occurrence recomputed after the cascade will not include it. A 200 means the competitor is gone; the cascade runs on the event bus and finishes shortly afterwards. The response body is the competitor as it was immediately before removal, because it can no longer be read back. The request takes no body, and sending one is refused. A competitor belonging to another project answers 404, the same answer an unknown id gets. An Idempotency-Key header is required.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique per attempt. A retry carrying the same key is answered from the record instead of running again."
              }
            ]
          }
        },
        {
          "name": "Restore an archived project",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/restore",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "restore"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: manager, in an active organization. Requires the \"write\" capability. Brings an archived project back to active, with every tracked query, capture and metric it had when it was archived. What is NOT done: no check is run and no scraping is scheduled as a result — collection resumes on the project's own schedule. Restoring clears the record of who archived the project, so a project restored here is treated as an ordinary active project by any later organization archive. A project archived because its organization was archived cannot be restored on its own: restore the organization, which restores them all. Restoring a project that is already active answers 409, unless the call is a retry carrying the key that restored it. This endpoint takes no body, and one carrying fields is refused. Send an Idempotency-Key; a retry with the same key returns the recorded answer.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ]
          }
        }
      ]
    },
    {
      "name": "Discovery",
      "item": [
        {
          "name": "Start a brand-name alias suggestion job",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/brand-name-suggestions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "brand-name-suggestions"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: manager. Starts a background, web-search-grounded job that suggests alias spellings for one entity - a brand, a project or a competitor - described entirely by the body, and answers 202 with the job to poll; it never returns suggestions inline. The entity need not exist: nothing is looked up, nothing is attached and nothing is saved, so the suggestions are the caller's to apply. Names sent in `enteredBrandNames` are excluded from the result. The organization must be active; no subscription is required, matching the application behaviour this mirrors. Ten starts per minute per organization, counted across every key of every member; a retry carrying an Idempotency-Key already answered is served from the record and does not count.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeat it to retry a start whose response was lost; the same key returns the same job instead of starting a second one."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Acme\",\n  \"websiteDomains\": [\n    \"https://acme.com\"\n  ],\n  \"enteredBrandNames\": [\n    \"Acme\",\n    \"Acme Inc\"\n  ],\n  \"country\": \"ES\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Start a brand discovery job for a project",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/discovery/brands",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "discovery",
                "brands"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": "",
                  "description": "Must belong to the organization in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Starts background discovery of the brands and competitors around a project and answers 202 with the job to poll; it never returns brands inline. The job PROPOSES names: it creates and updates nothing, so neither the brand profile nor the competitor list changes because this endpoint was called. The organization must be active. Unlike keyword and prompt discovery this operation does not require an entitled subscription, matching the application behaviour it mirrors. Ten starts per minute per organization, counted across every key of every member and shared with the same operation in the Mencoro app; a retry carrying an Idempotency-Key already answered is served from the record and does not count. A project whose brand monitoring profile has not been created yet is still accepted and still billed: the job runs grounded on the project name alone, with no website domains, brand names or competitors to work from, and completes normally. Configure the brand profile first if you want the discovery grounded.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeat it to retry a start whose response was lost; the same key returns the same job instead of starting a second one."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"shoppingEnabled\": false,\n  \"country\": \"ES\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Start a keyword discovery job for a project",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/discovery/keywords",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "discovery",
                "keywords"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": "",
                  "description": "Must belong to the organization in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Starts background keyword discovery from free-text seed input and answers 202 with the job to poll; it never returns keywords inline. The job SUGGESTS keywords - it creates no tracked queries and changes nothing in the project, so a completed job is a list to choose from, not work that has been applied. Queries the project already tracks are excluded from the suggestions automatically; `excludeQueries` adds more on top for this run only. The organization must be active and hold an entitled subscription, because the run spends provider budget. Ten starts per minute per organization, counted across every key of every member; a retry carrying an Idempotency-Key already answered is served from the record and does not count. Run tuning is not exposed: the discovery fan-out uses provider defaults.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeat it to retry a start whose response was lost; the same key returns the same job instead of starting a second one."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"input\": \"crm for plumbers, field service software\",\n  \"language\": \"es\",\n  \"country\": \"ES\",\n  \"excludeQueries\": [\n    \"\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Start a geo prompt discovery job for a project",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/projects/:projectId/discovery/prompts",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "projects",
                ":projectId",
                "discovery",
                "prompts"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "projectId",
                  "value": "",
                  "description": "Must belong to the organization in the path."
                }
              ]
            },
            "description": "Minimum role: manager. Starts background discovery of natural-language prompts - the questions an answer engine gets asked - from free-text seed input, and answers 202 with the job to poll; it never returns prompts inline. The job SUGGESTS prompts: it creates no tracked queries and changes nothing in the project. `country` is required here, unlike keyword discovery, because a prompt is a question asked from somewhere and the provider request has no default for it. Queries the project already tracks are excluded automatically; `excludeQueries` adds more for this run only. The organization must be active and hold an entitled subscription, because the run spends provider budget. Ten starts per minute per organization, counted across every key of every member; a retry carrying an Idempotency-Key already answered is served from the record and does not count. Run tuning is not exposed: the discovery fan-out uses provider defaults.",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Repeat it to retry a start whose response was lost; the same key returns the same job instead of starting a second one."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"input\": \"crm for plumbers, field service software\",\n  \"country\": \"ES\",\n  \"language\": \"es\",\n  \"excludeQueries\": [\n    \"\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "Invitations",
      "item": [
        {
          "name": "List an organization's invitations",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/invitations?limit=20&offset=0&search=&status=pending&sortBy=createdAt&sortOrder=desc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "invitations"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "",
                  "description": "Matches part of the invited email address.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "pending",
                  "description": "Absent means every state.",
                  "disabled": true
                },
                {
                  "key": "sortBy",
                  "value": "createdAt",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: owner. Returns the invitations issued for this organization, in every state. The invitation token is never returned. `status` matches the stored state, so an invitation that has passed its `expiresAt` is still listed as pending until it is transitioned; compare `expiresAt` to decide. Rows are ordered by the chosen sort field, and invitations sharing the same value keep no guaranteed relative order across pages.Send `Accept: text/csv` to receive the same page as a CSV download instead of JSON: same filters, same authorization, same page window and the same maximum of 100 rows — it is this page in another format, not a bulk export, so a whole collection is still read by paging. The CSV carries no `total`, because a table whose every row is a record has nowhere to put one; read it from the JSON representation of the same request. Cells beginning with `=`, `+`, `-` or `@` are prefixed with an apostrophe so a spreadsheet treats them as text rather than running them as formulas."
          }
        },
        {
          "name": "Invite somebody to an organization",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/invitations",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "invitations"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: owner. Sends an invitation email that expires after 14 days. If the address already belongs to an active member the call succeeds and creates nothing - answered as 200 with created=false rather than 201, so a client can tell the two apart. Preview it first; the preview says which of the two will happen.",
            "header": [
              {
                "key": "X-Mencoro-Confirmation",
                "value": ""
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"someone@example.com\",\n  \"role\": \"viewer\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Cancel a pending invitation",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/invitations/:invitationId/cancel",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "invitations",
                ":invitationId",
                "cancel"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "invitationId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: owner. The link already emailed to the invitee stops working. Only a pending invitation can be cancelled; one that was accepted, rejected or has expired is refused.",
            "header": [
              {
                "key": "X-Mencoro-Confirmation",
                "value": ""
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ]
          }
        }
      ]
    },
    {
      "name": "Jobs",
      "item": [
        {
          "name": "Get an asynchronous job",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/jobs/:jobId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "jobs",
                ":jobId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "jobId",
                  "value": "",
                  "description": "Must be a job started inside the organization in the path."
                }
              ]
            },
            "description": "Minimum role: manager. The status of one asynchronous job started inside this organization — geo prompt discovery, keyword discovery, query clustering, brand discovery or brand-name suggestion — and, once it has completed, its result. Poll it until `status` is terminal: `completed` or `failed`. A null `result` means the result is not known, either because the job is still in flight or because it failed; it never means the job produced an empty result. A failed job carries no reason: the stored one is an internal exception message, not a published field. A job started by another organization, a job of a type this API does not publish, and a job id that does not exist all answer 404 alike, so the API never confirms that an inaccessible job exists."
          }
        }
      ]
    },
    {
      "name": "Account",
      "item": [
        {
          "name": "Get the authenticated identity",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/me",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "me"
              ]
            },
            "description": "Returns the user the API key belongs to, plus the key's capabilities and scope. Use it to confirm which credential a call runs under."
          }
        },
        {
          "name": "Counts across everything the key can reach",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/me/stats",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "me",
                "stats"
              ]
            },
            "description": "Aggregate counts over every organization the key's owner is an active member of, narrowed to the key's scope — the same set /api/v1/organizations pages through. `organizations.total` is that set's size; `projects.total` and `projects.active` count the projects inside it, archived ones included in the total and excluded from the active figure. Every value is an exact count: zero means zero, and no value here is ever null or unknown. Per-organization billing and usage figures are not part of this response; read them from the subscription and entitlements operations instead."
          }
        }
      ]
    },
    {
      "name": "Members",
      "item": [
        {
          "name": "List an organization's members",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/members?limit=20&offset=0&status=active&sortBy=joinedAt&sortOrder=desc",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "members"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "active",
                  "description": "Absent means both states.",
                  "disabled": true
                },
                {
                  "key": "sortBy",
                  "value": "joinedAt",
                  "disabled": true
                },
                {
                  "key": "sortOrder",
                  "value": "desc",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                }
              ]
            },
            "description": "Minimum role: owner. The application exposes this roster twice and the two disagree — its member screen shows it to any viewer, while its non-BFF endpoint requires an owner — so the published API takes the stricter of the two and requires an owner. A membership describes the membership, not the person: names and email addresses are never returned here, and there is no search parameter, because both would turn the roster into a contact export. Omitting \"status\" returns active and suspended memberships alike. Sorting by \"role\" is alphabetical on the role name, not by seniority.Send `Accept: text/csv` to receive the same page as a CSV download instead of JSON: same filters, same authorization, same page window and the same maximum of 100 rows — it is this page in another format, not a bulk export, so a whole collection is still read by paging. The CSV carries no `total`, because a table whose every row is a record has nowhere to put one; read it from the JSON representation of the same request. Cells beginning with `=`, `+`, `-` or `@` are prefixed with an apostrophe so a spreadsheet treats them as text rather than running them as formulas."
          }
        },
        {
          "name": "Get one organization membership",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/members/:memberId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "members",
                ":memberId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "memberId",
                  "value": "",
                  "description": "The membership id, not the user id. Must belong to the organization in the path."
                }
              ]
            },
            "description": "Minimum role: owner — the same floor the members listing enforces, because a caller who can page the roster has already seen this record. Returns the facts of one membership: its role, whether it is active or suspended, and when it was joined. It does NOT describe the person behind it: no name, no email address, no phone number and no profile image, so a membership id can never be turned into a contact lookup. The membership is read from PostgreSQL, the same row at the same freshness the listing publishes. A membership belonging to another organization answers 404, exactly as an unknown or malformed id does, so the API never confirms that an inaccessible membership exists; the one 403 is a key without the read capability. The response carries no organizationId — it is the one in the path — and no creation timestamp; joinedAt is the membership fact. No query parameters are accepted."
          }
        },
        {
          "name": "Change a member role",
          "request": {
            "method": "PATCH",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/members/:memberId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "members",
                ":memberId"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "memberId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: owner. The role of a suspended member cannot be changed, and the last active owner cannot be demoted. Preview it first: the confirmation is bound to the number of active owners, so another owner being suspended in between invalidates it rather than stranding the organization.",
            "header": [
              {
                "key": "X-Mencoro-Confirmation",
                "value": ""
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"role\": \"owner\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Reactivate a suspended member",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/members/:memberId/reactivate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "members",
                ":memberId",
                "reactivate"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "memberId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: owner. The member keeps the role they had and regains access on their very next request. A member who is already active cannot be reactivated.",
            "header": [
              {
                "key": "X-Mencoro-Confirmation",
                "value": ""
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ]
          }
        },
        {
          "name": "Suspend a member",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organizations/:organizationId/members/:memberId/suspend",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organizations",
                ":organizationId",
                "members",
                ":memberId",
                "suspend"
              ],
              "variable": [
                {
                  "key": "organizationId",
                  "value": "{{organization_id}}"
                },
                {
                  "key": "memberId",
                  "value": ""
                }
              ]
            },
            "description": "Minimum role: owner. The member loses access on their very next request, including through any API key they own that is scoped to this organization. The last active owner cannot be suspended.",
            "header": [
              {
                "key": "X-Mencoro-Confirmation",
                "value": ""
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ]
          }
        }
      ]
    },
    {
      "name": "Organization operations",
      "item": [
        {
          "name": "Preview an organization operation and obtain a confirmation",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{base_url}}/organization-operation-previews",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "organization-operation-previews"
              ]
            },
            "description": "Organization, member and invitation changes need two calls. This one reports the concrete changes, the side effects, the warnings and the conditions, and returns a confirmation valid for five minutes. Send it back in the X-Mencoro-Confirmation header, together with an Idempotency-Key, on the call that performs the operation. Previewing has no effects of any kind.",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"action\": \"createOrganization\",\n  \"organizationId\": \"00000000-0000-0000-0000-000000000000\",\n  \"resourceId\": \"00000000-0000-0000-0000-000000000000\",\n  \"payload\": {}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    }
  ]
}
