Mencoro API

Everything Mencoro tracks is reachable over HTTP: projects, tracked queries, the raw captures behind every check, and the analytics computed from them. 90 operations , authenticated with an API key you issue yourself.

The OpenAPI contract is the source of truth, and it is public: reading it needs no key. Every operation below is rendered from that contract at build time, so this page cannot describe an API that no longer exists.

Client libraries

Official clients for seven languages, each generated from the same contract: github.com/mencoro/mencoro-api-sdk.

Language Install
Python 3.9+ pip install mencoro
TypeScript npm install @mencoro/api
PHP 8.1+ composer require mencoro/mencoro-api-sdk
Go 1.21+ go get github.com/mencoro/mencoro-api-sdk/go
Java 11+ com.mencoro:mencoro-api
.NET 8 dotnet add package Mencoro.Api
Ruby 3.0+ gem install mencoro

The snippets on this page use each language's own HTTP client, so they run with nothing installed. The SDKs are the shortcut once a project is past its first call.

Getting a key

Keys are issued from your account, under API keys in the user menu. Each key carries the capabilities it may exercise and the organizations it may reach.

The plaintext key is shown once, at creation, and never again. Store it where you store any other secret. If you lose it, revoke it and issue another; revocation takes effect on the very next request.

A key never widens what its owner can do. It only restricts: if your role in an organization is narrowed, or your membership is withdrawn, the key loses that access immediately, without being revoked and without being reissued.

Capabilities

Capability What it allows
read Every listing and every analytics operation. This is the floor: every key has it, so a key handed out for reporting cannot mutate anything.
write Projects, tracked queries, clusters, competitors and discovery jobs, still bounded by the role the owner holds in each organization.
organization:manage Members, invitations and the organization itself. Requested explicitly at creation: a key that can write product data cannot touch the organization without it.

Your first request

https://api.mencoro.com/api/v1/me returns the user the key belongs to, plus the key's capabilities and scope. It is the quickest way to confirm a key works and to see which organizations it reaches.

curl -X GET "https://api.mencoro.com/api/v1/me" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "userId": "00000000-0000-0000-0000-000000000000",
  "fullName": "",
  "email": "someone@example.com",
  "language": "en",
  "createdAt": "2026-01-01T00:00:00+00:00",
  "apiKeyId": "00000000-0000-0000-0000-000000000000",
  "capabilities": [
    "read"
  ],
  "scopeMode": "selected",
  "organizationIds": [
    "00000000-0000-0000-0000-000000000000"
  ]
}

Errors

Every refusal answers the same envelope, and every one carries a requestId. Quote it to support and it identifies your exact request.

{
  "code": "validation_error",
  "message": "The request failed validation.",
  "requestId": "01a0c58a-3370-7170-acdb-a0a7b760bc83",
  "details": {
    "limit": [
      { "code": "out_of_range", "message": "\"limit\" must be between 1 and 100." }
    ]
  }
}

details is keyed by field, and each entry carries one of seven codes: missing_field, invalid_type, invalid_value, invalid_uuid, invalid_email, out_of_range and unknown_field. An unrecognised parameter is reported rather than ignored, so a typo in a filter never silently hands you the unfiltered set.

The top-level code is the field to branch on, and it is stable. A refusal this API makes deliberately carries a name of its own: organization_not_found, project_is_archived, api_key_capability_required, idempotency_key_reused, confirmation_required, rate_limit_exceeded. Every operation lists the ones it can produce beside the status they come with.

Where no such name applies, the code is the status itself: bad_request for a body that is not JSON, not_found for a path that matches no route, method_not_allowed for the wrong verb. Any 5xx answers internal_error and nothing more specific. What failed inside is in our logs against your requestId, never in the body. A code never names the framework underneath, so it does not change when that does.

Limits

600 requests per user and 300 per key, every 60 seconds. The per-user ceiling is the real one: issuing more keys divides the same budget rather than adding to it.

Over budget answers 429 with a Retry-After header in seconds. Honour it rather than retrying immediately.

Writes are idempotent

Every write requires an Idempotency-Key header, 8 to 255 printable ASCII characters with no spaces, and a UUID is the obvious choice. Reuse it when you retry and the stored result is replayed rather than the operation running again, so a lost response never becomes a duplicate project or a second batch of tracked queries.

A key is scoped to the API key that used it, and bound to the request it first answered: method, path and body. Reusing it for a different request is refused with idempotency_key_reused rather than answering the wrong operation.

Organizational changes are confirmed

Some operations reach beyond your own data: archiving an organization, changing a member's role, inviting somebody. Those take two calls. First POST /api/v1/organization-operation-previews, which reports exactly what the operation would change and returns a confirmation.token. Then the operation itself, with that token in X-Mencoro-Confirmation.

The token is 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, the call is refused with 409 rather than doing more than was agreed. Without the header it answers 428.

Paging, dates and CSV

Listings take limit (default 20, maximum 100) and offset. A limit above the maximum is refused, never quietly clamped, so the page you asked for is the page you get. total counts everything the filter matches, not the size of the page.

Dates are YYYY-MM-DD and cover whole days in UTC.

Several listings also answer text/csv when you ask for it with an Accept header. That is the same page of the same rows, encoded differently: the 100-row maximum applies unchanged, because it is a page and not a bulk export.

Also available over MCP

To read the same data inside an AI client rather than from your own code, the Mencoro MCP server exposes it to Claude, ChatGPT, Cursor and anything else that speaks MCP. Same data, no code to write.

Tracked Queries

GET /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries

Search a project's tracked queries

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
limit query integer min 1, max 100, default 20 Page size. A value above the maximum is rejected, never clamped.
offset query integer min 0, default 0
search query string max length 100 Free-text search over the keyword.
status query active | paused
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated.
countries query string[] ISO-3166 alpha-2 codes or English names. Must be configured on the project.
sortBy query queryText | lastSerpPosition | lastMentionPosition | lastShoppingPosition | lastShareOfVoice | lastPositivityIndex | lastMentionCount | lastCheckedAt default "queryText"
sortOrder query asc | desc default "desc"

Responses

  • 200 The project's tracked queries
  • 400 A filter or page bound was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "id": null,
      "queryText": null,
      "engine": null,
      "country": null,
      "status": null,
      "queryClusterIds": null,
      "checkFrequency": null,
      "nPasses": null,
      "lastSerpPosition": null,
      "lastMentionPosition": null,
      "lastLinkPosition": null,
      "lastShoppingPosition": null,
      "lastShareOfVoice": null,
      "lastPositivityIndex": null,
      "lastPositiveMentionCount": null,
      "lastNeutralMentionCount": null,
      "lastNegativeMentionCount": null,
      "lastCheckedAt": null
    }
  ],
  "total": 0
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries

Create tracked queries

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string A client-chosen key, unique per operation. Replaying it returns the first answer instead of creating anything again.

Request body application/json, Required

Field Type Description
queryTexts Required string[] Prompts or keywords to track. Stored lower-cased with whitespace collapsed and leading list markers removed.
engines Required chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Engines each text is asked in.
countries Required string[] ISO 3166-1 alpha-2 countries each text is asked from.
locale string min length 2, max length 2 ISO 639-1 language the queries are asked in. Omit it to ask the engine without a language.
checkFrequency Required daily | weekly | monthly How often every created query is checked.
nPasses Required integer min 1, max 10 Passes run per check. Applied to AI-engine combinations only: a non-AI engine is always created with 1, whatever is sent here.
queryClusterIds Required string (uuid)[] Clusters every created query joins. Must already exist in the project. A combination that is already tracked has these clusters merged into it.

Responses

  • 200 Per-combination results. Read failed: a non-empty failed with a 200 is the normal partial-success answer.
  • 400 The body was rejected, or the Idempotency-Key header is missing; details names the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or query cluster the caller can access under these ids
  • 409 The organization is archived, the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
  • 422 A query cluster exists but belongs to another project
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "queryTexts": [
    ""
  ],
  "engines": [
    "chatgpt"
  ],
  "countries": [
    ""
  ],
  "locale": "",
  "checkFrequency": "daily",
  "nPasses": 1,
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ]
}'

Example response

{
  "successful": [
    {
      "id": null,
      "queryText": null,
      "engine": null,
      "country": null
    }
  ],
  "failed": [
    {
      "queryText": null,
      "engine": null,
      "country": null,
      "errorCode": null,
      "errorMessage": null
    }
  ]
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}

Get a tracked query

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid) Must belong to the project in the path.

Responses

  • 200 The tracked query
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization, project or tracked query the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "queryText": "",
  "engine": "chatgpt",
  "locale": "",
  "country": "",
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "status": "active",
  "checkFrequency": "daily",
  "nPasses": 1,
  "lastCheckedAt": "2026-01-01T00:00:00+00:00"
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}/check-frequency

Change how often a tracked query is checked

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid) Must belong to the project in the path.
Idempotency-Key Required header string A client-chosen key, unique per operation. Replaying it returns the first answer instead of applying anything again.

Request body application/json, Required

Field Type Description
checkFrequency Required daily | weekly | monthly How often the query is checked while it is active.

Responses

  • 200 The tracked query in its new state
  • 400 The body was rejected, or the Idempotency-Key header is missing; details names the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or tracked query the caller can access under these ids
  • 409 The organization is archived, the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID/check-frequency" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "checkFrequency": "daily"
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "queryText": "",
  "engine": "chatgpt",
  "locale": "",
  "country": "",
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "status": "active",
  "checkFrequency": "daily",
  "nPasses": 1,
  "lastCheckedAt": "2026-01-01T00:00:00+00:00"
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}/clusters

Add a tracked query to clusters

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid) Must belong to the project in the path.
Idempotency-Key Required header string A client-chosen key, unique per operation, so a lost response can be retried without repeating the write.

Request body application/json, Required

Field Type Description
queryClusterIds Required string (uuid)[] Ids of the query clusters. Duplicates are collapsed. Every id must belong to the project in the path.

Responses

  • 200 The tracked query, including the clusters it now belongs to
  • 400 The body was rejected: an unknown field, a malformed cluster id, a cluster outside this project, or a missing Idempotency-Key
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or tracked query the caller can access under these ids
  • 409 The organization is archived, the project is archived, the idempotency key was reused for a different body, or a cluster was deleted between validation and the write, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID/clusters" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ]
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "queryText": "",
  "engine": "chatgpt",
  "locale": "",
  "country": "",
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "status": "active",
  "checkFrequency": "daily",
  "nPasses": 1,
  "lastCheckedAt": "2026-01-01T00:00:00+00:00"
}

DELETE /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}/clusters

Remove a tracked query from clusters

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid) Must belong to the project in the path.
Idempotency-Key Required header string A client-chosen key, unique per operation, so a lost response can be retried without repeating the write.

Request body application/json, Required

Field Type Description
queryClusterIds Required string (uuid)[] Ids of the query clusters. Duplicates are collapsed. Every id must belong to the project in the path.

Responses

  • 200 The tracked query, including the clusters it still belongs to
  • 400 The body was rejected or missing: an unknown field, a malformed cluster id, a cluster outside this project, a stripped DELETE body, or a missing Idempotency-Key
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or tracked query the caller can access under these ids
  • 409 The organization is archived, the project is archived, the idempotency key was reused for a different body, or a cluster was deleted between validation and the write, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X DELETE "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID/clusters" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ]
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "queryText": "",
  "engine": "chatgpt",
  "locale": "",
  "country": "",
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "status": "active",
  "checkFrequency": "daily",
  "nPasses": 1,
  "lastCheckedAt": "2026-01-01T00:00:00+00:00"
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}/mention-matches

List stored mention matches of a tracked query

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid)
dateFrom query string (date) Inclusive UTC day; defaults to the retention floor.
dateTo query string (date) Inclusive UTC day.
limit query integer min 1, max 100, default 20
offset query integer min 0, default 0
sortOrder query asc | desc default "desc"

Responses

  • 200 Stored matches and total under the same filters
  • 400 Invalid or unsupported query parameters
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 Organization, project or tracked query is not accessible
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID/mention-matches" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "trackedQueryId": null,
      "projectId": null,
      "engine": null,
      "aiResponseId": null,
      "competitorId": null,
      "mentionPosition": null,
      "sentiment": null,
      "mentionType": null,
      "mentionTypeCondition": null,
      "responseContext": null,
      "detectedAt": null,
      "mentionRelation": null,
      "brandName": null,
      "brandNameAsMentioned": null
    }
  ],
  "total": 0
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}/passes

Change how many passes a tracked query runs per check

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid) Must belong to the project in the path.
Idempotency-Key Required header string A client-chosen key, unique per operation. Replaying it returns the first answer instead of applying anything again.

Request body application/json, Required

Field Type Description
nPasses Required integer min 1, max 10 Passes run per check. Only an AI engine accepts more than one.

Responses

  • 200 The tracked query in its new state
  • 400 The body was rejected, or the Idempotency-Key header is missing; details names the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or tracked query the caller can access under these ids
  • 409 The engine runs a single pass and cannot take more; or the organization or project is archived; or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID/passes" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "nPasses": 1
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "queryText": "",
  "engine": "chatgpt",
  "locale": "",
  "country": "",
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "status": "active",
  "checkFrequency": "daily",
  "nPasses": 1,
  "lastCheckedAt": "2026-01-01T00:00:00+00:00"
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}/pause

Pause a tracked query

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid) Must belong to the project in the path.
Idempotency-Key Required header string Repeating a request with the same key answers with the first attempt's result instead of pausing again.

Responses

  • 200 The tracked query, paused
  • 400 The Idempotency-Key header is missing or malformed, or the request carried a body
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or tracked query the caller can access under these ids
  • 409 The organization or the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID/pause" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "queryText": "",
  "engine": "chatgpt",
  "locale": "",
  "country": "",
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "status": "active",
  "checkFrequency": "daily",
  "nPasses": 1,
  "lastCheckedAt": "2026-01-01T00:00:00+00:00"
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}/responses/{aiResponseId}/report

Report a problem with a captured AI answer

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid) Must belong to the project in the path.
aiResponseId Required path string (uuid) A capture id from the AI responses listing. Must belong to the tracked query in the path.
Idempotency-Key Required header string A client-chosen key, unique per report, so a lost response can be retried without filing the report twice.

Request body application/json, Required

Field Type Description
type Required missed_mention | other What is wrong with the capture.
comment string max length 1000 Optional note for the reviewer, at most 1000 characters. An empty string is stored as no comment.
missedBrandNames string[] Brands the engine mentioned that the pipeline did not record. Most useful with type "missed_mention"; accepted with either. At most 50 distinct names of 255 characters each.

Responses

  • 200 The report as accepted, in its normalised form
  • 400 The body was rejected: an unknown field, a missing or unsupported "type", an over-long comment or brand name, or a missing Idempotency-Key
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project, tracked query or capture the caller can access under these ids
  • 409 The organization is archived, the capture can no longer be routed back to its scrape run, the idempotency key was reused for a different body, or a previous attempt with this key ended with an unknown outcome
  • 502 The review system could not be reached or refused the forward. Whether the report was filed is UNKNOWN: delivery is at-most-once and the failure can happen after the report was recorded upstream. This idempotency key will not replay; retrying with a new key may file the report twice.
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID/responses/$AI_RESPONSE_ID/report" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "missed_mention",
  "comment": "",
  "missedBrandNames": [
    ""
  ]
}'

Example response

{
  "aiResponseId": "00000000-0000-0000-0000-000000000000",
  "trackedQueryId": "00000000-0000-0000-0000-000000000000",
  "type": "missed_mention",
  "comment": "",
  "missedBrandNames": [
    ""
  ],
  "accepted": false
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}/resume

Resume a tracked query

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid) Must belong to the project in the path.
Idempotency-Key Required header string Repeating a request with the same key answers with the first attempt's result instead of resuming again.

Responses

  • 200 The tracked query, active
  • 400 The Idempotency-Key header is missing or malformed, or the request carried a body
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or tracked query the caller can access under these ids
  • 409 The organization or the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID/resume" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "queryText": "",
  "engine": "chatgpt",
  "locale": "",
  "country": "",
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "status": "active",
  "checkFrequency": "daily",
  "nPasses": 1,
  "lastCheckedAt": "2026-01-01T00:00:00+00:00"
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}/serp-matches

List stored serp matches of a tracked query

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid)
dateFrom query string (date) Inclusive UTC day; defaults to the retention floor.
dateTo query string (date) Inclusive UTC day.
limit query integer min 1, max 100, default 20
offset query integer min 0, default 0
sortOrder query asc | desc default "desc"

Responses

  • 200 Stored matches and total under the same filters
  • 400 Invalid or unsupported query parameters
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 Organization, project or tracked query is not accessible
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID/serp-matches" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "trackedQueryId": null,
      "projectId": null,
      "engine": null,
      "searchPageId": null,
      "competitorId": null,
      "position": null,
      "detectedAt": null
    }
  ],
  "total": 0
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/batch/check-frequency

Change how often several tracked queries are checked

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string A client-chosen key, unique per operation. Replaying it returns the first answer instead of applying anything again.

Request body application/json, Required

Field Type Description
ids Required string (uuid)[] Ids of the tracked queries to change. Duplicates are collapsed.
checkFrequency Required daily | weekly | monthly How often each query is checked while it is active.

Responses

  • 200 Per-id results. Read failed: a non-empty failed with a 200 is the normal partial-success answer.
  • 400 The body was rejected, or the Idempotency-Key header is missing; details names the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The organization is archived, the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/batch/check-frequency" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "checkFrequency": "daily"
}'

Example response

{
  "successful": [
    {
      "id": null
    }
  ],
  "failed": [
    {
      "id": null,
      "errorCode": null,
      "errorMessage": null
    }
  ]
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/batch/passes

Change how many passes several tracked queries run per check

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string A client-chosen key, unique per operation. Replaying it returns the first answer instead of applying anything again.

Request body application/json, Required

Field Type Description
ids Required string (uuid)[] Ids of the tracked queries to change. Duplicates are collapsed.
nPasses Required integer min 1, max 10 Passes run per check. Only an AI engine accepts more than one; a non-AI target is reported under "failed".

Responses

  • 200 Per-id results. Read failed: a non-empty failed with a 200 is the normal partial-success answer.
  • 400 The body was rejected, or the Idempotency-Key header is missing; details names the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The organization is archived, the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/batch/passes" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "nPasses": 1
}'

Example response

{
  "successful": [
    {
      "id": null
    }
  ],
  "failed": [
    {
      "id": null,
      "errorCode": null,
      "errorMessage": null
    }
  ]
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/batch/pause

Pause several tracked queries

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string Repeating a request with the same key and the same ids answers with the first attempt's result.

Request body application/json, Required

Field Type Description
ids Required string (uuid)[] Ids of the resources to act on. Duplicates are collapsed.

Responses

  • 200 Per-item results. failed is always present, empty list included: read it rather than inferring success from the status.
  • 400 The Idempotency-Key header is missing, the body carried an unknown field, or "ids" was absent, malformed or larger than 100 distinct entries
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The organization or the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/batch/pause" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": [
    "00000000-0000-0000-0000-000000000000"
  ]
}'

Example response

{
  "successful": [
    {
      "id": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "failed": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "errorCode": "tracked_query_not_found",
      "errorMessage": ""
    }
  ]
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/batch/resume

Resume several tracked queries

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string Repeating a request with the same key and the same ids answers with the first attempt's result.

Request body application/json, Required

Field Type Description
ids Required string (uuid)[] Ids of the resources to act on. Duplicates are collapsed.

Responses

  • 200 Per-item results. failed is always present, empty list included: read it rather than inferring success from the status.
  • 400 The Idempotency-Key header is missing, the body carried an unknown field, or "ids" was absent, malformed or larger than 100 distinct entries
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The organization or the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/batch/resume" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": [
    "00000000-0000-0000-0000-000000000000"
  ]
}'

Example response

{
  "successful": [
    {
      "id": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "failed": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "errorCode": "tracked_query_not_found",
      "errorMessage": ""
    }
  ]
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/bulk-delete

Delete tracked queries

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string A client-chosen key, unique per operation. Replaying it returns the first answer instead of deleting anything again.

Request body application/json, Required

Field Type Description
ids Required string (uuid)[] Ids of the resources to act on. Duplicates are collapsed.

Responses

  • 200 Per-id results. Read failed: a non-empty failed with a 200 is the normal partial-success answer.
  • 400 The body was rejected, or the Idempotency-Key header is missing; details names the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The organization is archived, the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/bulk-delete" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": [
    "00000000-0000-0000-0000-000000000000"
  ]
}'

Example response

{
  "successful": [
    {
      "id": null
    }
  ],
  "failed": [
    {
      "id": null,
      "errorCode": null,
      "errorMessage": null
    }
  ]
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/bulk/clusters

Add many tracked queries to clusters

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string A client-chosen key, unique per operation, so a lost response can be retried without repeating the batch.

Request body application/json, Required

Field Type Description
ids string (uuid)[] Tracked queries to add. At most 100 distinct ids; duplicates are collapsed.
queryClusterIds string (uuid)[] Clusters every named tracked query is added to. All must belong to the project in the path.

Responses

  • 200 Per-item results. Read "failed": a 200 does not mean every item succeeded.
  • 400 The body was rejected: an unknown field, a malformed or missing id, more than 100 distinct ids, a cluster outside this project, or a missing Idempotency-Key
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The organization is archived, the project is archived, or the idempotency key was reused for a different body, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/bulk/clusters" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ]
}'

Example response

{
  "successful": [
    {
      "id": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "failed": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "errorCode": "tracked_query_not_found",
      "errorMessage": ""
    }
  ]
}

DELETE /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/bulk/clusters

Remove many tracked queries from clusters

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string A client-chosen key, unique per operation, so a lost response can be retried without repeating the batch.

Request body application/json, Required

Field Type Description
ids string (uuid)[] Tracked queries to remove. At most 100 distinct ids; duplicates are collapsed.
queryClusterIds string (uuid)[] Clusters every named tracked query is removed from. All must belong to the project in the path.

Responses

  • 200 Per-item results. Read "failed": a 200 does not mean every item succeeded.
  • 400 The body was rejected or missing: an unknown field, a malformed or missing id, more than 100 distinct ids, a cluster outside this project, a stripped DELETE body, or a missing Idempotency-Key
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The organization is archived, the project is archived, or the idempotency key was reused for a different body, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X DELETE "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/bulk/clusters" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "queryClusterIds": [
    "00000000-0000-0000-0000-000000000000"
  ]
}'

Example response

{
  "successful": [
    {
      "id": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "failed": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "errorCode": "tracked_query_not_found",
      "errorMessage": ""
    }
  ]
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/check

Check several tracked queries now

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string Repeating a request with the same key and the same ids answers with the first attempt's result instead of submitting again.

Request body application/json, Required

Field Type Description
ids Required string (uuid)[] Ids of the resources to act on. Duplicates are collapsed.

Responses

  • 200 Per-item results. failed is always present, empty list included: read it rather than inferring success from the status.
  • 400 The Idempotency-Key header is missing, the body carried an unknown field, or "ids" was absent, malformed or larger than 100 distinct entries
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The organization or the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/check" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": [
    "00000000-0000-0000-0000-000000000000"
  ]
}'

Example response

{
  "successful": [
    {
      "id": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "failed": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "errorCode": "check_budget_forecast_exhausted",
      "errorMessage": ""
    }
  ]
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/check-all

Check every eligible tracked query of a project now

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string Repeating a request with the same key answers with the first attempt's result instead of submitting a second wave.

Responses

  • 200 The tracked queries submitted for a check
  • 400 The Idempotency-Key header is missing or malformed, or the request carried a body
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The organization or the project is archived, or the idempotency key was already used for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/check-all" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "submitted": 0,
  "trackedQueryIds": [
    "00000000-0000-0000-0000-000000000000"
  ]
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/count

Count a project's tracked queries and price checking them

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
status query active | paused Restrict both numbers to one status. Lower-case; an unknown value is rejected, not ignored. Omitted, every status is counted and priced.

Responses

  • 200 The tracked query count and the budget cost of checking that set
  • 400 The status parameter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/count" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "count": 42,
  "checkCost": 96
}

Analytics

GET /api/v1/metric-glossary

Map everyday wording to a metric and the operation that serves it

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.

Responses

  • 200 The metric catalogue
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
curl -X GET "https://api.mencoro.com/api/v1/metric-glossary" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "metrics": [
    {
      "metric": "",
      "aliases": [],
      "unit": "",
      "range": "",
      "direction": "higher_is_better",
      "operation": "",
      "exampleQuestions": []
    }
  ]
}

GET /api/v1/organizations/{organizationId}/overview

Snapshot rank-health board across an organization active projects

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)

Responses

  • 200 Per-project snapshot rows and the organization aggregate
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller can access under this id
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/overview" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "projects": [
    {
      "projectId": "00000000-0000-0000-0000-000000000000",
      "name": "",
      "status": "",
      "trackedQueryCount": 0,
      "shareOfVoice": 0,
      "mentionRate": 0,
      "avgMentionPosition": 0,
      "positivityIndex": 0
    }
  ],
  "aggregate": {
    "projectCount": 0,
    "projectsWithData": 0,
    "totalTrackedQueries": 0,
    "avgShareOfVoice": 0,
    "avgMentionRate": 0,
    "avgMentionPosition": 0,
    "avgPositivityIndex": 0
  }
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/available-filters

Filter values a project is configured for

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)

Responses

  • 200 The engines, countries and clusters configured on the project
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/available-filters" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "engines": [
    ""
  ],
  "countries": [
    ""
  ],
  "clusters": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "name": ""
    }
  ]
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/cited-sources

Domains and pages the AI answers cited

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom Required query string (date) Inclusive start of the window, Y-m-d. Must fall inside the data retention window.
dateTo Required query string (date) Inclusive end of the window, Y-m-d.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated. Only the AI engines carry citations.
groupBy query domain | page default "domain" Grain of the roll-up: "domain" by host, "page" by exact URL.
limit query integer min 1, max 100, default 20 Page size. A larger value is rejected, never silently reduced.
offset query integer min 0, default 0 Number of sources to skip.

Responses

  • 200 The cited sources for this page, plus the total number of distinct sources in the window
  • 400 A filter or page bound was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/cited-sources?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "sources": [],
  "total": 0
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/competitors/co-occurrence

Head-to-head record of the brand against each tracked competitor

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom Required query string (date) Inclusive start of the window, Y-m-d. Must fall inside the data retention window.
dateTo Required query string (date) Inclusive end of the window, Y-m-d.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated. SERP and Shopping carry no AI answer text, so they contribute no co-occurrence.
countries query string[] ISO-3166 alpha-2 codes or English names. Must be configured on the project.
competitorId query string (uuid) Restricts the answer to a single tracked competitor. Omit it for every tracked competitor. The available-filters endpoint lists the valid ids.

Responses

  • 200 One head-to-head row per tracked competitor, under "competitors"
  • 400 A filter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/competitors/co-occurrence?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "competitors": []
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/coverage

Coverage and staleness of a project tracked queries

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)

Responses

  • 200 Coverage counts and a sample of the most-overdue queries
  • 400 The request was rejected before it reached the project; this endpoint accepts no query parameters, so no filter can be refused here
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/coverage" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "projectId": "00000000-0000-0000-0000-000000000000",
  "total": 0,
  "active": 0,
  "paused": 0,
  "neverChecked": 0,
  "overdue": 0,
  "sample": [
    {
      "trackedQueryId": "00000000-0000-0000-0000-000000000000",
      "queryText": "",
      "engine": "",
      "country": "",
      "checkFrequency": "daily",
      "lastCheckedAt": "2026-01-01T00:00:00+00:00"
    }
  ]
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/keyword-listings

List a project's keywords with their windowed metrics

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom Required query string (date) Inclusive start of the window, Y-m-d. Must fall inside the data retention window.
dateTo Required query string (date) Inclusive end of the window, Y-m-d. The trend compares against the equally long window ending the day before dateFrom.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated. Narrows which variants count towards each row.
countries query string[] ISO-3166 alpha-2 codes or English names. Must be configured on the project.
queryClusterIds query string (uuid)[] Restrict to keywords with a variant in these clusters. Each must belong to the project.
includeUngroupedQueries query boolean default false Sent alone, restricts the listing to keywords whose variants belong to no cluster; sent with queryClusterIds, adds them to that selection.
status query active | paused Restrict to variants with this status. A keyword whose variants disagree still reports statusSummary "mixed".
checkFrequencies query daily | weekly | monthly[] Repeatable, or comma-separated.
nPasses query integer[] Repeatable, or comma-separated. Restrict to variants configured with these pass counts.
search query string max length 100 Case-insensitive substring match on the keyword text.
sortBy query keyword | variantCount | lastCheckedAt | statusSummary | positivityIndex | shareOfVoice | avgMentionPosition | avgLinkPosition | mentionPositionStability | avgSerpPosition | avgShoppingPosition default "keyword" 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.
sortOrder query asc | desc default "asc"
limit query integer min 1, max 100, default 20 Page size. A larger value is rejected, never silently reduced.
offset query integer min 0, default 0 Number of matching keywords to skip before the page starts.

Responses

  • 200 A page of keywords, the totals behind it, and whether the metrics are pending recalculation
  • 400 A filter, sort key or page bound was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/keyword-listings?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "keyword": null,
      "keywordNormalized": null,
      "variantCount": null,
      "variantIds": null,
      "engines": null,
      "countries": null,
      "queryClusterIds": null,
      "hasUnclusteredVariant": null,
      "statusSummary": null,
      "statuses": null,
      "checkFrequencies": null,
      "nPassesValues": null,
      "lastCheckedAt": null,
      "avgSerpPosition": null,
      "trendSerp": null,
      "avgShoppingPosition": null,
      "trendShopping": null,
      "avgMentionPosition": null,
      "trendMention": null,
      "avgLinkPosition": null,
      "trendLink": null,
      "mentionPositionStability": null,
      "trendStability": null,
      "positivityIndex": null,
      "trendPositivity": null,
      "mentionRate": null,
      "trendMentionRate": null,
      "serpRate": null,
      "trendSerpRate": null,
      "shareOfVoice": null,
      "trendShareOfVoice": null,
      "sentimentPositive": null,
      "sentimentNeutral": null,
      "sentimentNegative": null,
      "avgMentionCount": null,
      "mentionTypeCounts": null
    }
  ],
  "total": 0,
  "totalVariantCount": 0,
  "dataDirtySince": "2026-01-01T00:00:00+00:00"
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/mentions

Sample of the raw AI mention texts of a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom Required query string (date) Inclusive start of the window, Y-m-d. Must fall inside the data retention window.
dateTo Required query string (date) Inclusive end of the window, Y-m-d.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated. Non-AI engines contribute no mentions.
countries query string[] ISO-3166 alpha-2 codes or English names. Must be configured on the project.
sentiment query positive | neutral | negative Restrict to one sentiment label. Omit for every sentiment.
mentionType query recommendation | comparison | listing | example | reference Restrict to one mention type. Omit for every type.
competitorId query string (uuid) 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.
sortBy query recent | negative | engine | country default "recent" 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.
limit query integer min 1, max 100, default 20 Page size. A larger value is rejected, never silently reduced.
offset query integer min 0, default 0 Number of matching mentions to skip before the page starts.

Responses

  • 200 A page of mention samples and the total matching the filters
  • 400 A filter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/mentions?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "samples": [],
  "total": 0
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/mentions/mix

Composition of a project brand mentions in AI answers

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom Required query string (date) Inclusive start of the window, Y-m-d. Must fall inside the data retention window.
dateTo Required query string (date) Inclusive end of the window, Y-m-d.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated. Non-AI engines contribute no mentions.
countries query string[] ISO-3166 alpha-2 codes or English names. Must be configured on the project.

Responses

  • 200 Mention counts by type, by tone and by qualifier
  • 400 A filter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/mentions/mix?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "byType": {},
  "byTone": {},
  "byQualifier": {}
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/metrics

Headline visibility metrics of a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom Required query string (date) Inclusive start of the window, Y-m-d. Must fall inside the data retention window.
dateTo Required query string (date) Inclusive end of the window, Y-m-d.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated.
countries query string[] ISO-3166 alpha-2 codes or English names. Must be configured on the project.
queryClusterIds query string (uuid)[] Restrict to these keyword clusters. Each must belong to this project.
includeUngroupedQueries query boolean default false Only meaningful together with queryClusterIds: also counts the tracked queries that belong to no cluster.

Responses

  • 200 Headline metrics, trends, sentiment split, per-competitor share of voice and position distributions
  • 400 A filter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/metrics?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "avgSerpPosition": 0,
  "trendSerp": 0,
  "avgShoppingPosition": 0,
  "trendShopping": 0,
  "avgMentionPosition": 0,
  "trendMention": 0,
  "avgLinkPosition": 0,
  "trendLink": 0,
  "mentionPositionStability": 0,
  "trendStability": 0,
  "serpPositionStability": 0,
  "trendSerpStability": 0,
  "shoppingPositionStability": 0,
  "trendShoppingStability": 0,
  "positivityIndex": 0,
  "trendPositivity": 0,
  "mentionRate": 0,
  "trendMentionRate": 0,
  "serpRate": 0,
  "trendSerpRate": 0,
  "shoppingRate": 0,
  "trendShoppingRate": 0,
  "shareOfVoice": 0,
  "trendShareOfVoice": 0,
  "sentimentPositive": 0,
  "sentimentNeutral": 0,
  "sentimentNegative": 0,
  "mentionCount": 0,
  "aiTrackedQueryCount": 0,
  "aiQueriesWithMention": 0,
  "serpTrackedQueryCount": 0,
  "serpQueriesWithResult": 0,
  "shoppingTrackedQueryCount": 0,
  "shoppingQueriesWithResult": 0,
  "mentionTypeCounts": {
    "recommendation": null,
    "comparison": null,
    "listing": null,
    "example": null,
    "reference": null
  },
  "dataDirtySince": null,
  "mentionPositionDistribution": [],
  "serpPositionDistribution": [],
  "shoppingPositionDistribution": [],
  "competitorShareOfVoice": []
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/metrics/clusters

Rank-tracking metrics per keyword cluster

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom Required query string (date) Inclusive start of the window, Y-m-d. Must fall inside the data retention window.
dateTo Required query string (date) Inclusive end of the window, Y-m-d.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated.
countries query string[] ISO-3166 alpha-2 codes or English names. Must be configured on the project.
queryClusterIds query string (uuid)[] Restrict to these clusters. Each must belong to the project.
includeUngroupedQueries query boolean default false Sent alone, returns only the ungrouped bucket rather than adding it to every cluster.

Responses

  • 200 One row per keyword cluster, plus the ungrouped bucket where it applies
  • 400 A filter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/metrics/clusters?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "rows": [],
  "dataDirtySince": null
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/metrics/movers

Tracked queries ranked by how much a metric moved

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom Required query string (date) 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.
dateTo Required query string (date) Inclusive end of the window, Y-m-d.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated.
countries query string[] ISO-3166 alpha-2 codes or English names. Must be configured on the project.
sortBy query trend_serp | trend_shopping | trend_mention | trend_link | trend_share_of_voice default "trend_share_of_voice" 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.
sortOrder query asc | desc default "desc" desc for the top gainers, asc for the top losers.
limit query integer min 1, max 100, default 20 Page size. A value above the maximum is rejected, never clamped.
offset query integer min 0, default 0

Responses

  • 200 The ranked tracked queries under "rows", with "total" and "dataDirtySince"
  • 400 A filter or page bound was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/metrics/movers?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "rows": [],
  "total": 0,
  "dataDirtySince": null
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/metrics/share-of-voice-formula

The constants behind the Share of Voice score

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)

Responses

  • 200 The Share of Voice weighting constants
  • 400 Not reachable here: the endpoint accepts no query parameters, so it has nothing to reject. Listed because the error envelope is shared across the API
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/metrics/share-of-voice-formula" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "mentionTypeWeights": {},
  "sentimentMultipliers": {},
  "directMultiplier": 0,
  "conditionalMultiplier": 0
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/sentiment

Sentiment breakdown of a project brand mentions

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom Required query string (date) Inclusive start of the window, Y-m-d. Must fall inside the data retention window.
dateTo Required query string (date) Inclusive end of the window, Y-m-d.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated.
countries query string[] ISO-3166 alpha-2 codes or English names. Must be configured on the project.
queryClusterIds query string (uuid)[]
includeUngroupedQueries query boolean default false

Responses

  • 200 Sentiment per engine and per competitor
  • 400 A filter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/sentiment?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "perEngine": [],
  "perCompetitor": []
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/timeseries

Rank-tracking metrics of a project over time

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom Required query string (date) Inclusive start of the window, Y-m-d. Must fall inside the data retention window.
dateTo Required query string (date) Inclusive end of the window, Y-m-d.
granularity query daily | weekly | monthly default "daily" Bucket size of each point.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode | google_serp | google_shopping[] Repeatable, or comma-separated.
countries query string[] ISO-3166 alpha-2 codes or English names. Must be configured on the project.
queryClusterIds query string (uuid)[]
includeUngroupedQueries query boolean default false 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.
competitorIds query string (uuid)[] Repeatable, or comma-separated. Each id adds one series under the competitors map of every point.

Responses

  • 200 Points in bucket order, plus dataDirtySince: a date from which the rank data is being recomputed, or null when nothing is pending
  • 400 A filter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/timeseries?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "points": [],
  "dataDirtySince": null
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/tracked-queries/{trackedQueryId}/timeseries

Rank-tracking time series of a single tracked query

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
trackedQueryId Required path string (uuid) Must belong to the project in the path.
dateFrom Required query string (date) Inclusive start of the window, Y-m-d. Must fall inside the data retention window.
dateTo Required query string (date) Inclusive end of the window, Y-m-d.
granularity query daily | weekly | monthly default "daily" Bucket size. Prefer weekly or monthly for long windows.
competitorIds query string (uuid)[] Competitors to add as extra series, repeatable or comma-separated. Valid ids come from the available-filters endpoint.

Responses

  • 200 One point per bucket, with brand and per-competitor metrics
  • 400 A filter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization, project or tracked query the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/tracked-queries/$TRACKED_QUERY_ID/timeseries?dateFrom=2026-01-01&dateTo=2026-01-01" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "points": [],
  "dataDirtySince": null
}

Organizations

GET /api/v1/organizations

List accessible organizations

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.

Parameters

Name In Type Description
limit query integer min 1, max 100, default 20
offset query integer min 0, default 0
search query string max length 100
status query active | archived
sortBy query createdAt | name default "createdAt"
sortOrder query asc | desc default "desc"

Responses

  • 200 Accessible organizations
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
curl -X GET "https://api.mencoro.com/api/v1/organizations" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "id": null,
      "name": null,
      "description": null,
      "status": null,
      "imageUrl": null,
      "contactEmail": null,
      "role": null,
      "createdAt": null
    }
  ],
  "total": 0
}

POST /api/v1/organizations

Create an organization

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.

Parameters

Name In Type Description
X-Mencoro-Confirmation Required header string
Idempotency-Key Required header string

Request body application/json, Required

Field Type Description
name string min length 3, max length 100

Responses

  • 201 Organization created. Your owner membership follows asynchronously, so a read immediately afterwards can answer 404 until it lands.
  • 401 Missing or invalid API key
  • 403 The key is not scoped to all organizations, or lacks "organization:manage"
  • 409 The confirmation is invalid, expired, already used, or its effects changed
  • 428 No confirmation was sent; preview the operation first
curl -X POST "https://api.mencoro.com/api/v1/organizations" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "X-Mencoro-Confirmation: $X_MENCORO_CONFIRMATION" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": ""
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "status": ""
}

GET /api/v1/organizations/{organizationId}

Get an organization

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)

Responses

  • 200 The organization
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller can access under this id
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "description": "",
  "status": "active",
  "imageUrl": "",
  "contactEmail": "someone@example.com",
  "role": "owner",
  "createdAt": "2026-01-01T00:00:00+00:00"
}

PATCH /api/v1/organizations/{organizationId}

Update an organization profile

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
X-Mencoro-Confirmation Required header string
Idempotency-Key Required header string

Request body application/json, Required

Field Type Description
name string min length 3, max length 100
description string
contactEmail string (email)

Responses

  • 200 The updated organization
  • 401 Missing or invalid API key
  • 403 The key lacks the organization:manage capability
  • 404 No organization the caller can access under this id
  • 409 The confirmation is invalid, expired, already used, or the organization changed since the preview
  • 428 No confirmation was sent; preview the operation first
curl -X PATCH "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "X-Mencoro-Confirmation: $X_MENCORO_CONFIRMATION" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "",
  "description": "",
  "contactEmail": "someone@example.com"
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "description": "",
  "status": "active",
  "imageUrl": "",
  "contactEmail": "someone@example.com",
  "role": "owner",
  "createdAt": "2026-01-01T00:00:00+00:00"
}

POST /api/v1/organizations/{organizationId}/archive

Archive an organization

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
X-Mencoro-Confirmation Required header string 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.
Idempotency-Key Required header string min length 8, max length 255 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.

Responses

  • 200 The organization in its new state
  • 401 Missing or invalid API key
  • 403 The key lacks the organization:manage capability
  • 404 No organization the caller can access under this id
  • 409 The confirmation is invalid, expired, already used, or its declared effects changed; or the status transition is not allowed
  • 428 No confirmation was sent; preview the operation first
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/archive" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "X-Mencoro-Confirmation: $X_MENCORO_CONFIRMATION" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "description": "",
  "status": "active",
  "imageUrl": "",
  "contactEmail": "someone@example.com",
  "role": "owner",
  "createdAt": "2026-01-01T00:00:00+00:00"
}

GET /api/v1/organizations/{organizationId}/entitlements

Get an organization's plan allowance and consumption

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)

Responses

  • 200 The organization's allowance and consumption
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller owns under this id
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/entitlements" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "status": "active",
  "isEntitled": false,
  "checkBudget": 0,
  "checksConsumed": 0,
  "checksAvailable": 0,
  "billingCycleType": "monthly",
  "billingCycleAnchor": "2026-01-01T00:00:00+00:00",
  "nextResetAt": "2026-01-01T00:00:00+00:00",
  "cancelledAt": "2026-01-01T00:00:00+00:00",
  "scheduledToCancelAt": "2026-01-01T00:00:00+00:00",
  "gracePeriodEndsAt": "2026-01-01T00:00:00+00:00"
}

GET /api/v1/organizations/{organizationId}/membership-stats

Membership, project and invitation counts for an organization

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)

Responses

  • 200 The organization's membership snapshot
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller owns under this id (code organization_not_found), or the snapshot has not been computed for it yet (code organization_membership_stats_not_found)
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/membership-stats" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "activeMembersCount": 85,
  "suspendedMembersCount": 3,
  "totalMembersCount": 88,
  "activeProjectsCount": 18,
  "totalProjectsCount": 25,
  "pendingInvitationsCount": 5,
  "computedAt": "2026-02-12 10:30:00"
}

POST /api/v1/organizations/{organizationId}/restore

Restore an archived organization

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
X-Mencoro-Confirmation Required header string
Idempotency-Key Required header string

Responses

  • 200 The organization in its new state
  • 401 Missing or invalid API key
  • 403 The key lacks the organization:manage capability
  • 404 No organization the caller can access under this id
  • 409 The confirmation is invalid, expired, already used, or its declared effects changed; or the status transition is not allowed
  • 428 No confirmation was sent; preview the operation first
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/restore" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "X-Mencoro-Confirmation: $X_MENCORO_CONFIRMATION" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "description": "",
  "status": "active",
  "imageUrl": "",
  "contactEmail": "someone@example.com",
  "role": "owner",
  "createdAt": "2026-01-01T00:00:00+00:00"
}

GET /api/v1/organizations/{organizationId}/stats

Headline counts for an organization

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)

Responses

  • 200 The organization counts
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller can access under this id
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/stats" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "members": {
    "total": 12
  },
  "projects": {
    "total": 25,
    "active": 18
  },
  "invitations": {
    "pending": 3
  }
}

GET /api/v1/organizations/{organizationId}/usage/projected-monthly-checks

Project a month of check consumption from the current tracking configuration

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)

Responses

  • 200 The projected monthly checks and the active tracked queries behind them
  • 400 A query parameter was sent to an operation that accepts none; the details name it
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller can access under this id
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/usage/projected-monthly-checks" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "projectedMonthlyChecks": 720,
  "activeTrackedQueryCount": 24
}

GET /api/v1/organizations/{organizationId}/usage/tracked-queries

Count the tracked queries an organization has configured

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)

Responses

  • 200 The organization's tracked-query count
  • 400 A query parameter was sent to an operation that accepts none; the details name it
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller can access under this id
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/usage/tracked-queries" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "trackedQueryCount": 124
}

Billing

GET /api/v1/organizations/{organizationId}/subscription

Get the subscription of an organization

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)

Responses

  • 200 The subscription contract, or the "none" state
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller can access under this id
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/subscription" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "status": "active",
  "tierCode": "tier_2000",
  "billingCycleType": "monthly",
  "billingCycleAnchor": "2026-01-01T00:00:00+00:00",
  "checkBudget": 0,
  "checksConsumed": 0,
  "checksAvailable": 0,
  "nextResetAt": "2026-01-01T00:00:00+00:00",
  "cancelledAt": "2026-01-01T00:00:00+00:00",
  "scheduledToCancelAt": "2026-01-01T00:00:00+00:00",
  "gracePeriodEndsAt": "2026-01-01T00:00:00+00:00",
  "deactivationReason": "payment_failed",
  "isEntitled": false
}

Captures

GET /api/v1/organizations/{organizationId}/projects/{projectId}/ai-responses

List captured AI answers

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom query string (date) Inclusive lower bound, widened to 00:00:00 UTC of the named day. Must be inside the retention window.
dateTo query string (date) Inclusive upper bound, widened to 23:59:59 UTC of the named day.
trackedQueryId query string (uuid) Only captures of this tracked query. Must belong to the project in the path.
engines query chatgpt | perplexity | google_ai_overview | google_ai_mode[] Only captures from these AI engines. Repeat the parameter or pass a comma-separated list. An empty filter means every engine.
limit query integer min 1, max 100, default 20 Page size. A larger value is rejected, never silently reduced.
offset query integer min 0, default 0 Number of captures to skip before the page starts.
sortOrder query asc | desc default "desc" Direction of the capture-time ordering. An unknown value is rejected, not replaced by the default.

Responses

  • 200 A page of captured AI answers and the total the filter matches
  • 400 A filter, pagination or ordering parameter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/ai-responses" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "id": null,
      "projectId": null,
      "trackedQueryId": null,
      "engine": null,
      "responseText": null,
      "citations": null,
      "capturedAt": null,
      "modelName": null,
      "passIndex": null,
      "passCount": null
    }
  ],
  "total": 0
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/search-snapshots

List captured search-results pages

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom query string (date) Inclusive lower bound, widened to 00:00:00 UTC of the named day. Must be inside the retention window.
dateTo query string (date) Inclusive upper bound, widened to 23:59:59 UTC of the named day.
trackedQueryId query string (uuid) Only captures of this tracked query. Must belong to the project in the path.
limit query integer min 1, max 100, default 20 Page size. A larger value is rejected, never silently reduced.
offset query integer min 0, default 0 Number of captures to skip before the page starts.
sortOrder query asc | desc default "desc" Direction of the capture-time ordering. An unknown value is rejected, not replaced by the default.

Responses

  • 200 A page of captured search-results pages and the total the filter matches
  • 400 A filter, pagination or ordering parameter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/search-snapshots" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "id": null,
      "projectId": null,
      "trackedQueryId": null,
      "engine": null,
      "results": null,
      "capturedAt": null
    }
  ],
  "total": 0
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/shopping-snapshots

List captured shopping-results pages

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
dateFrom query string (date) 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.
dateTo query string (date) Inclusive upper bound, widened to 23:59:59 UTC of the named day.
trackedQueryId query string (uuid) Only captures of this tracked query. Must belong to the project in the path.
limit query integer min 1, max 100, default 20 Page size. A larger value is rejected, never silently reduced.
offset query integer min 0, default 0 Number of captures to skip before the page starts.
sortOrder query asc | desc default "desc" Direction of the capture-time ordering. An unknown value is rejected, not replaced by the default.

Responses

  • 200 A page of captured shopping-results pages and the total the filter matches
  • 400 A filter, pagination or ordering parameter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/shopping-snapshots" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "id": null,
      "projectId": null,
      "trackedQueryId": null,
      "engine": null,
      "offers": null,
      "capturedAt": null
    }
  ],
  "total": 0
}

Clusters

POST /api/v1/organizations/{organizationId}/projects/{projectId}/clusters

Create a keyword cluster

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string min length 8, max length 255 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.

Request body application/json, Required

Field Type Description
name string max length 200 Stored trimmed and lower-cased. Unique within the project.

Responses

  • 201 The cluster that was created
  • 400 The body was rejected, or the Idempotency-Key header is missing; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 A cluster with this name already exists in the project, the organization is archived, the project is archived, or the idempotency key was reused for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/clusters" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": ""
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "name": ""
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/clusters/{clusterId}

Get one of a project's keyword clusters

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid) Must belong to the organization in the path.
clusterId Required path string (uuid) Must be a cluster of the project in the path.

Responses

  • 200 The keyword cluster
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization, project or cluster the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/clusters/$CLUSTER_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "name": ""
}

PATCH /api/v1/organizations/{organizationId}/projects/{projectId}/clusters/{clusterId}

Rename a keyword cluster

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
clusterId Required path string (uuid) Must be a cluster of the project in the path.
Idempotency-Key Required header string Repeat it to retry a lost response without renaming twice.

Request body application/json, Required

Field Type Description
name string max length 200 Stored trimmed and lower-cased. Unique within the project.

Responses

  • 200 The cluster in its new state
  • 400 The body was rejected, or the Idempotency-Key header is missing; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or cluster the caller can access under these ids
  • 409 Another cluster in the project already has this name, the organization is archived, the project is archived, or the idempotency key was reused for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PATCH "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/clusters/$CLUSTER_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": ""
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "projectId": "00000000-0000-0000-0000-000000000000",
  "name": ""
}

DELETE /api/v1/organizations/{organizationId}/projects/{projectId}/clusters/{clusterId}

Delete a keyword cluster

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
clusterId Required path string (uuid) Must be a cluster of the project in the path.
Idempotency-Key Required header string Repeat it to retry a lost response; a fresh key answers 404 once the cluster is gone.

Responses

  • 200 The cluster was deleted
  • 400 A body was sent, or the Idempotency-Key header is missing; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or cluster the caller can access under these ids
  • 409 The organization is archived, or the idempotency key was reused for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X DELETE "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/clusters/$CLUSTER_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "deleted": false
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/clusters/batch

Create several keyword clusters at once

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string Repeat it to retry a lost response without creating the batch twice.

Request body application/json, Required

Field Type Description
names Required string[] Cluster names to create, already trimmed and lower-cased on the server. Duplicates are collapsed.

Responses

  • 200 Per-name results; read failed rather than inferring success from the status
  • 400 The body was rejected, or the Idempotency-Key header is missing; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The organization is archived, or the idempotency key was reused for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/clusters/batch" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "names": [
    ""
  ]
}'

Example response

{
  "successful": [
    {
      "name": null,
      "id": null
    }
  ],
  "failed": [
    {
      "name": null,
      "errorCode": null,
      "errorMessage": null
    }
  ]
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/clusters/jobs

Start a keyword clustering job

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string Repeat it to retry a lost response without starting a second job.

Request body application/json, Required

Field Type Description
trackedQueryIds Required string (uuid)[] The tracked queries to cluster. Duplicates are collapsed.
mode Required fill_gaps | full_regroup | add_on_top fill_gaps groups only tracked queries that belong to no cluster; add_on_top adds the new clusters to whatever each query already has; full_regroup replaces the current clusters with the job's.
restrictToExistingClusters boolean default false When true the job may only use clusters the project already has, and leaves a query ungrouped rather than inventing a name for it.

Responses

  • 202 The job was accepted; poll trackingUrl until its status is terminal
  • 400 The body was rejected, or the Idempotency-Key header is missing; the details name the field
  • 401 Missing or invalid API key
  • 402 The organization has no entitled subscription
  • 403 The key lacks the write capability
  • 404 No organization, project or tracked query the caller can access under these ids
  • 409 The organization is archived, the project is archived, there is nothing to cluster, or the idempotency key was reused for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
  • 429 More than 10 clustering starts in a minute for this organization; Retry-After says when to try again
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/clusters/jobs" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "trackedQueryIds": [
    "00000000-0000-0000-0000-000000000000"
  ],
  "mode": "fill_gaps",
  "restrictToExistingClusters": false
}'

Example response

{
  "jobId": "00000000-0000-0000-0000-000000000000",
  "deduplicated": false,
  "trackingUrl": ""
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/clusters/jobs/{jobId}/apply

Apply the result of a clustering job

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
jobId Required path string (uuid) Must be a clustering job started for this organization and project.
Idempotency-Key Required header string Repeat it to retry a lost response without applying the job twice.

Responses

  • 200 What was written; read failed rather than inferring success from the status
  • 400 A body was sent, or the Idempotency-Key header is missing; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or clustering job the caller can access under these ids
  • 409 The job is not completed, produced nothing that can be applied, the organization is archived, the project is archived, or the idempotency key was reused for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/clusters/jobs/$JOB_ID/apply" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "successful": [
    {
      "trackedQueryId": null,
      "queryClusterIds": null
    }
  ],
  "failed": [
    {
      "trackedQueryId": null,
      "errorCode": null,
      "errorMessage": null
    }
  ],
  "clusters": [
    {
      "id": null,
      "name": null,
      "created": null
    }
  ],
  "skippedClusters": [
    {
      "name": null,
      "reason": null
    }
  ],
  "unassigned": [
    "00000000-0000-0000-0000-000000000000"
  ]
}

Projects

GET /api/v1/organizations/{organizationId}/projects

List an organization's projects

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
limit query integer min 1, max 100, default 20 Page size. A larger value is rejected, never silently reduced.
offset query integer min 0, default 0 Rows to skip before the page starts. Page by advancing it in steps of limit until it reaches total.
search query string max length 100
status query active | archived
sortBy query createdAt | name | trackedQueryCount | avgSerpPosition | avgShoppingPosition | avgMentionPosition | avgLinkPosition | mentionRate | serpRate | shoppingRate | positivityIndex | shareOfVoice | serpPositionStability | shoppingPositionStability | lastRankDetectedAt default "createdAt"
sortOrder query asc | desc default "desc"

Responses

  • 200 The organization's projects
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller can access under this id
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "id": null,
      "name": null,
      "status": null,
      "createdAt": null,
      "trackedQueryCount": null,
      "avgSerpPosition": null,
      "avgShoppingPosition": null,
      "avgMentionPosition": null,
      "avgLinkPosition": null,
      "mentionRate": null,
      "serpRate": null,
      "shoppingRate": null,
      "positivityIndex": null,
      "shareOfVoice": null,
      "serpPositionStability": null,
      "shoppingPositionStability": null,
      "lastRankDetectedAt": null
    }
  ],
  "total": 0
}

POST /api/v1/organizations/{organizationId}/projects

Create a project and the brand monitoring profile its checks run against

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
Idempotency-Key Required header string min length 8, max length 255

Request body application/json, Required

Field Type Description
name Required string min length 1, max length 255
websiteDomains Required string[] At least one website URL or domain to monitor.
brandNames Required string[] At least one brand name to match in AI answers and search results.
competitors object[] Competitors to create with the project. Optional; they can also be added later through the competitor endpoints.

Responses

  • 201 The created project and its brand monitoring configuration
  • 400 The body or the Idempotency-Key was rejected; details names every field at fault
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization the caller can access under this id
  • 409 The organization is archived, the idempotency key was already used for a different request, or a previous attempt with it ended without a known outcome
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ],
  "competitors": [
    {
      "name": "",
      "websiteDomains": [],
      "brandNames": []
    }
  ]
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "organizationId": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "status": "active",
  "createdAt": "2026-01-01T00:00:00+00:00",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ],
  "competitors": [
    {
      "id": null,
      "name": null,
      "websiteDomains": null,
      "brandNames": null
    }
  ]
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}

Get a project and its brand monitoring configuration

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)

Responses

  • 200 The project and its brand monitoring configuration
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller can access under this id, or no project with this id inside it
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "organizationId": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "status": "active",
  "createdAt": "2026-01-01T00:00:00+00:00",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ],
  "competitors": [
    {
      "id": null,
      "name": null,
      "websiteDomains": null,
      "brandNames": null
    }
  ]
}

PATCH /api/v1/organizations/{organizationId}/projects/{projectId}

Rename a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string min length 8, max length 255

Request body application/json, Required

Field Type Description
name Required string min length 1, max length 255

Responses

  • 200 The project in its new state
  • 400 The body or the Idempotency-Key was rejected; details names every field at fault
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The project is archived, the organization is archived, the idempotency key was already used for a different request, or a previous attempt with it ended without a known outcome
curl -X PATCH "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": ""
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "organizationId": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "status": "active",
  "createdAt": "2026-01-01T00:00:00+00:00",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ],
  "competitors": [
    {
      "id": null,
      "name": null,
      "websiteDomains": null,
      "brandNames": null
    }
  ]
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/archive

Archive a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string min length 8, max length 255

Responses

  • 200 The project in its new state
  • 400 A body was sent, or the Idempotency-Key was rejected
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The project is already archived, the organization is archived, the idempotency key was already used for a different request, or a previous attempt with it ended without a known outcome
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/archive" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "organizationId": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "status": "active",
  "createdAt": "2026-01-01T00:00:00+00:00",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ],
  "competitors": [
    {
      "id": null,
      "name": null,
      "websiteDomains": null,
      "brandNames": null
    }
  ]
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/brand-profile

Get a project's brand monitoring profile

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid) Must belong to the organization in the path.

Responses

  • 200 The project's brand monitoring profile
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids, or the project has no brand profile
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/brand-profile" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "projectId": "00000000-0000-0000-0000-000000000000",
  "brandNames": [
    ""
  ],
  "websiteDomains": [
    ""
  ],
  "description": ""
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/brand-profile

Replace a project's brand monitoring profile

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid) Must belong to the organization in the path.
Idempotency-Key Required header string Unique per attempt. A retry carrying the same key and the same body is answered from the record instead of running again.

Request body application/json, Required

Field Type Description
websiteDomains string[] Domains a cited link or a search result is matched against. A full URL or a bare host.
brandNames string[] Terms a mention in an AI answer is matched against.

Responses

  • 200 The profile as stored
  • 400 The body failed validation, or the Idempotency-Key header is missing
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids, or the project has no brand profile
  • 409 The organization is archived, the project is archived, or the idempotency key was reused for a different body, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/brand-profile" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ]
}'

Example response

{
  "projectId": "00000000-0000-0000-0000-000000000000",
  "brandNames": [
    ""
  ],
  "websiteDomains": [
    ""
  ],
  "description": ""
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/clusters

List the keyword clusters of a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
limit query integer min 1, max 100, default 20 Page size. A larger value is rejected, never silently reduced.
offset query integer min 0, default 0 Number of clusters to skip before the page starts.
sortOrder query asc | desc default "asc" Direction of the name ordering. An unknown value is rejected, not replaced by the default.

Responses

  • 200 A page of the project's keyword clusters and the total in the project
  • 400 A pagination or ordering parameter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/clusters" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "id": null,
      "projectId": null,
      "name": null
    }
  ],
  "total": 0
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/competitors

List the competitors tracked by a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
limit query integer min 1, max 100, default 20 Page size. A larger value is rejected, never silently reduced.
offset query integer min 0, default 0 Number of competitors to skip before the page starts.
sortOrder query asc | desc default "asc" Direction of the id ordering. An unknown value is rejected, not replaced by the default.

Responses

  • 200 The project's competitors
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or project the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/competitors" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "id": null,
      "name": null,
      "websiteDomains": null,
      "brandNames": null
    }
  ],
  "total": 0
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/competitors

Add a competitor to a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid) Must belong to the organization in the path.
Idempotency-Key Required header string Unique per attempt. Without it a lost response cannot be retried without risking a second competitor.

Request body application/json, Required

Field Type Description
name string max length 200 The competitor's display name
websiteDomains string[] Domains a result is matched against for this competitor. A full URL or a bare host.
brandNames string[] Names a mention is matched against for this competitor

Responses

  • 201 The competitor as stored
  • 400 The body failed validation, or the Idempotency-Key header is missing
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids, or the project has no brand monitoring profile
  • 409 The organization is archived, the project is archived, or the idempotency key was reused for a different body, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/competitors" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ]
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ]
}

GET /api/v1/organizations/{organizationId}/projects/{projectId}/competitors/{competitorId}

Get one of a project's competitors

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid) Must belong to the organization in the path.
competitorId Required path string (uuid) Must belong to the project in the path.

Responses

  • 200 The competitor
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization, project or competitor the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/competitors/$COMPETITOR_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ]
}

PUT /api/v1/organizations/{organizationId}/projects/{projectId}/competitors/{competitorId}

Replace a competitor

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid) Must belong to the organization in the path.
competitorId Required path string (uuid) Must belong to the project in the path.
Idempotency-Key Required header string Unique per attempt. A retry carrying the same key and the same body is answered from the record instead of running again.

Request body application/json, Required

Field Type Description
name string max length 200 The competitor's display name
websiteDomains string[] Domains a result is matched against for this competitor. A full URL or a bare host.
brandNames string[] Names a mention is matched against for this competitor

Responses

  • 200 The competitor as stored
  • 400 The body failed validation, or the Idempotency-Key header is missing
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or competitor the caller can access under these ids
  • 409 The organization is archived, the project is archived, or the idempotency key was reused for a different body, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X PUT "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/competitors/$COMPETITOR_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ]
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ]
}

DELETE /api/v1/organizations/{organizationId}/projects/{projectId}/competitors/{competitorId}

Remove a competitor from a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid) Must belong to the organization in the path.
competitorId Required path string (uuid) Must belong to the project in the path.
Idempotency-Key Required header string Unique per attempt. A retry carrying the same key is answered from the record instead of running again.

Responses

  • 200 The competitor as it was immediately before removal
  • 400 A body was sent, or the Idempotency-Key header is missing
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization, project or competitor the caller can access under these ids
  • 409 The organization is archived, the project is archived, or the idempotency key was reused for a different request, a concurrent request with the same key is still running, or a previous attempt with it ended without a known outcome
curl -X DELETE "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/competitors/$COMPETITOR_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ]
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/restore

Restore an archived project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid)
Idempotency-Key Required header string min length 8, max length 255

Responses

  • 200 The project in its new state
  • 400 A body was sent, or the Idempotency-Key was rejected
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The project is not archived, the organization is archived, the idempotency key was already used for a different request, or a previous attempt with it ended without a known outcome
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/restore" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "organizationId": "00000000-0000-0000-0000-000000000000",
  "name": "",
  "status": "active",
  "createdAt": "2026-01-01T00:00:00+00:00",
  "websiteDomains": [
    ""
  ],
  "brandNames": [
    ""
  ],
  "competitors": [
    {
      "id": null,
      "name": null,
      "websiteDomains": null,
      "brandNames": null
    }
  ]
}

Discovery

POST /api/v1/organizations/{organizationId}/brand-name-suggestions

Start a brand-name alias suggestion job

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
Idempotency-Key Required header string Repeat it to retry a start whose response was lost; the same key returns the same job instead of starting a second one.

Request body application/json, Required

Field Type Description
name Required string max length 255 The display name of the entity to find aliases for.
websiteDomains Required string[] The entity website domains. At least one is required: the web search is grounded on them, and without one the name alone is ambiguous. Duplicates are collapsed, after the entry count has been checked against maxItems.
enteredBrandNames string[] Names already known, excluded from the suggestions. Duplicates are collapsed, after the entry count has been checked against maxItems.
country string min length 2, max length 2 ISO 3166-1 alpha-2 country used to localize the web search.

Responses

  • 202 The suggestion job was accepted
  • 400 The body is not a JSON object, names an unknown field, or fails validation; or the Idempotency-Key header is missing
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization the caller can access under this id
  • 409 The organization is archived, or the Idempotency-Key was already used for a different request, or a previous attempt with it ended without a known outcome
  • 429 The organization exceeded ten suggestion starts per minute, or the key exceeded its request budget
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/brand-name-suggestions" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Acme",
  "websiteDomains": [
    "https://acme.com"
  ],
  "enteredBrandNames": [
    "Acme",
    "Acme Inc"
  ],
  "country": "ES"
}'

Example response

{
  "jobId": "00000000-0000-0000-0000-000000000000",
  "deduplicated": false,
  "trackingUrl": ""
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/discovery/brands

Start a brand discovery job for a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid) Must belong to the organization in the path.
Idempotency-Key Required header string Repeat it to retry a start whose response was lost; the same key returns the same job instead of starting a second one.

Request body application/json, Optional

Field Type Description
shoppingEnabled boolean default false Adds a shopping discovery pass. Set it only for a project that tracks a shopping engine: on any other project the pass spends provider budget on results nothing will use.
country string min length 2, max length 2 ISO 3166-1 alpha-2 country used to localize the discovery.

Responses

  • 202 The discovery job was accepted
  • 400 The body is not a JSON object, names an unknown field, or fails validation; or the Idempotency-Key header is missing
  • 401 Missing or invalid API key
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The project is archived, or the organization is archived, or the Idempotency-Key was already used for a different request, or a previous attempt with it ended without a known outcome
  • 429 The organization exceeded ten brand discovery starts per minute, or the key exceeded its request budget
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/discovery/brands" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "shoppingEnabled": false,
  "country": "ES"
}'

Example response

{
  "jobId": "00000000-0000-0000-0000-000000000000",
  "deduplicated": false,
  "trackingUrl": ""
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/discovery/keywords

Start a keyword discovery job for a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid) Must belong to the organization in the path.
Idempotency-Key Required header string Repeat it to retry a start whose response was lost; the same key returns the same job instead of starting a second one.

Request body application/json, Required

Field Type Description
input Required string max length 10000 Free text describing the keywords or topics to expand.
language string max length 5 Language code. Omit to let the provider detect it from the input.
country string min length 2, max length 2 ISO 3166-1 alpha-2 country. Omit to let the provider infer it.
excludeQueries string[] Extra queries to keep out of the suggestions for this run. Duplicates are collapsed, after the entry count has been checked against maxItems. The project tracked queries are excluded whether or not this is sent.

Responses

  • 202 The discovery job was accepted
  • 400 The body is not a JSON object, names an unknown field, or fails validation; or the Idempotency-Key header is missing
  • 401 Missing or invalid API key
  • 402 The organization has no entitled subscription
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The project is archived, or the organization is archived, or the Idempotency-Key was already used for a different request, or a previous attempt with it ended without a known outcome
  • 429 The organization exceeded ten discovery starts per minute, or the key exceeded its request budget
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/discovery/keywords" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "input": "crm for plumbers, field service software",
  "language": "es",
  "country": "ES",
  "excludeQueries": [
    ""
  ]
}'

Example response

{
  "jobId": "00000000-0000-0000-0000-000000000000",
  "deduplicated": false,
  "trackingUrl": ""
}

POST /api/v1/organizations/{organizationId}/projects/{projectId}/discovery/prompts

Start a geo prompt discovery job for a project

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
projectId Required path string (uuid) Must belong to the organization in the path.
Idempotency-Key Required header string Repeat it to retry a start whose response was lost; the same key returns the same job instead of starting a second one.

Request body application/json, Required

Field Type Description
input Required string max length 10000 Free text describing the topics to turn into prompts.
country Required string min length 2, max length 2 ISO 3166-1 alpha-2 country the prompts are asked from.
language string max length 5 Language code. Omit to let the provider detect it from the input.
excludeQueries string[] Extra queries to keep out of the suggestions for this run. Duplicates are collapsed, after the entry count has been checked against maxItems. The project tracked queries are excluded whether or not this is sent.

Responses

  • 202 The discovery job was accepted
  • 400 The body is not a JSON object, names an unknown field, or fails validation; or the Idempotency-Key header is missing
  • 401 Missing or invalid API key
  • 402 The organization has no entitled subscription
  • 403 The key lacks the write capability
  • 404 No organization or project the caller can access under these ids
  • 409 The project is archived, or the organization is archived, or the Idempotency-Key was already used for a different request, or a previous attempt with it ended without a known outcome
  • 429 The organization exceeded ten discovery starts per minute, or the key exceeded its request budget
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/projects/$PROJECT_ID/discovery/prompts" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "input": "crm for plumbers, field service software",
  "country": "ES",
  "language": "es",
  "excludeQueries": [
    ""
  ]
}'

Example response

{
  "jobId": "00000000-0000-0000-0000-000000000000",
  "deduplicated": false,
  "trackingUrl": ""
}

Invitations

GET /api/v1/organizations/{organizationId}/invitations

List an organization's invitations

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
limit query integer min 1, max 100, default 20
offset query integer min 0, default 0
search query string max length 100 Matches part of the invited email address.
status query pending | accepted | rejected | expired | cancelled Absent means every state.
sortBy query createdAt | expiresAt default "createdAt"
sortOrder query asc | desc default "desc"

Responses

  • 200 The organization's invitations
  • 400 A parameter was rejected; the details name the field
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller owns under this id
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/invitations" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "id": null,
      "email": null,
      "role": null,
      "state": null,
      "createdAt": null,
      "expiresAt": null,
      "acceptedAt": null
    }
  ],
  "total": 0
}

POST /api/v1/organizations/{organizationId}/invitations

Invite somebody to an organization

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
X-Mencoro-Confirmation Required header string
Idempotency-Key Required header string

Request body application/json, Required

Field Type Description
email string (email)
role owner | manager | viewer default "viewer"

Responses

  • 200 Nothing was created because the address already belongs to an active member. Not an error: the end state the caller asked for already holds.
  • 201 The invitation that was created. created is true; branch on it rather than on the status code, so one code path handles both answers.
  • 401 Missing or invalid API key
  • 403 The key lacks the organization:manage capability
  • 404 No organization the caller can access under this id
  • 409 The confirmation is invalid, expired, already used, or its effects changed; or a pending invitation already exists for this address
  • 428 No confirmation was sent; preview the operation first
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/invitations" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "X-Mencoro-Confirmation: $X_MENCORO_CONFIRMATION" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "someone@example.com",
  "role": "viewer"
}'

Example response

{
  "created": false,
  "reason": "address_is_already_a_member",
  "email": "someone@example.com"
}

POST /api/v1/organizations/{organizationId}/invitations/{invitationId}/cancel

Cancel a pending invitation

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
invitationId Required path string (uuid)
X-Mencoro-Confirmation Required header string
Idempotency-Key Required header string

Responses

  • 200 The invitation in its new state
  • 401 Missing or invalid API key
  • 403 The key lacks the organization:manage capability
  • 404 No organization or invitation the caller can access under these ids
  • 409 The confirmation is invalid, expired, already used, or its effects changed; or the invitation is not pending
  • 428 No confirmation was sent; preview the operation first
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/invitations/$INVITATION_ID/cancel" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "X-Mencoro-Confirmation: $X_MENCORO_CONFIRMATION" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "email": "someone@example.com",
  "role": "owner",
  "state": "pending",
  "createdAt": "2026-01-01T00:00:00+00:00",
  "expiresAt": "2026-01-01T00:00:00+00:00",
  "acceptedAt": "2026-01-01T00:00:00+00:00"
}

Jobs

GET /api/v1/organizations/{organizationId}/jobs/{jobId}

Get an asynchronous job

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
jobId Required path string (uuid) Must be a job started inside the organization in the path.

Responses

  • 200 The job, and its result once it has completed
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization or job the caller can access under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/jobs/$JOB_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "jobId": "00000000-0000-0000-0000-000000000000",
  "type": "geo_prompts",
  "status": "pending",
  "result": {}
}

Account

GET /api/v1/me

Get the authenticated identity

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.

Responses

  • 200 The authenticated identity
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
curl -X GET "https://api.mencoro.com/api/v1/me" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "userId": "00000000-0000-0000-0000-000000000000",
  "fullName": "",
  "email": "someone@example.com",
  "language": "en",
  "createdAt": "2026-01-01T00:00:00+00:00",
  "apiKeyId": "00000000-0000-0000-0000-000000000000",
  "capabilities": [
    "read"
  ],
  "scopeMode": "selected",
  "organizationIds": [
    "00000000-0000-0000-0000-000000000000"
  ]
}

GET /api/v1/me/stats

Counts across everything the key can reach

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.

Responses

  • 200 Counts across the organizations the key can reach
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability, or the user it belongs to is no longer active
curl -X GET "https://api.mencoro.com/api/v1/me/stats" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "organizations": {
    "total": 3
  },
  "projects": {
    "total": 25,
    "active": 18
  }
}

Members

GET /api/v1/organizations/{organizationId}/members

List an organization's members

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
limit query integer min 1, max 100, default 20
offset query integer min 0, default 0
status query active | suspended Absent means both states.
sortBy query joinedAt | role default "joinedAt"
sortOrder query asc | desc default "desc"

Responses

  • 200 The organization's memberships
  • 400 A parameter was rejected; the details name the field. A limit above 100 is refused, not clamped
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller owns under this id
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/members" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "items": [
    {
      "id": null,
      "userId": null,
      "role": null,
      "state": null,
      "isActive": null,
      "joinedAt": null
    }
  ],
  "total": 0
}

GET /api/v1/organizations/{organizationId}/members/{memberId}

Get one organization membership

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
memberId Required path string (uuid) The membership id, not the user id. Must belong to the organization in the path.

Responses

  • 200 The membership
  • 401 Missing or invalid API key
  • 403 The key lacks the read capability
  • 404 No organization the caller owns, or no membership of it, under these ids
curl -X GET "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/members/$MEMBER_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "userId": "00000000-0000-0000-0000-000000000000",
  "role": "owner",
  "state": "active",
  "isActive": false,
  "joinedAt": "2026-01-01T00:00:00+00:00"
}

PATCH /api/v1/organizations/{organizationId}/members/{memberId}

Change a member role

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
memberId Required path string (uuid)
X-Mencoro-Confirmation Required header string
Idempotency-Key Required header string

Request body application/json, Required

Field Type Description
role owner | manager | viewer

Responses

  • 200 The membership in its new state
  • 401 Missing or invalid API key
  • 403 The key lacks the organization:manage capability
  • 404 No organization or membership the caller can access under these ids
  • 409 The confirmation is invalid, expired, already used, or its effects changed; the member is suspended; or this is the last active owner
  • 428 No confirmation was sent; preview the operation first
curl -X PATCH "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/members/$MEMBER_ID" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "X-Mencoro-Confirmation: $X_MENCORO_CONFIRMATION" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "role": "owner"
}'

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "userId": "00000000-0000-0000-0000-000000000000",
  "role": "owner",
  "state": "active",
  "isActive": false,
  "joinedAt": "2026-01-01T00:00:00+00:00"
}

POST /api/v1/organizations/{organizationId}/members/{memberId}/reactivate

Reactivate a suspended member

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
memberId Required path string (uuid)
X-Mencoro-Confirmation Required header string
Idempotency-Key Required header string

Responses

  • 200 The membership in its new state
  • 401 Missing or invalid API key
  • 403 The key lacks the organization:manage capability
  • 404 No organization or membership the caller can access under these ids
  • 409 The confirmation is invalid, expired, already used, or its effects changed; or the member is already active
  • 428 No confirmation was sent; preview the operation first
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/members/$MEMBER_ID/reactivate" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "X-Mencoro-Confirmation: $X_MENCORO_CONFIRMATION" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "userId": "00000000-0000-0000-0000-000000000000",
  "role": "owner",
  "state": "active",
  "isActive": false,
  "joinedAt": "2026-01-01T00:00:00+00:00"
}

POST /api/v1/organizations/{organizationId}/members/{memberId}/suspend

Suspend a member

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.

Parameters

Name In Type Description
organizationId Required path string (uuid)
memberId Required path string (uuid)
X-Mencoro-Confirmation Required header string
Idempotency-Key Required header string

Responses

  • 200 The membership in its new state
  • 401 Missing or invalid API key
  • 403 The key lacks the organization:manage capability
  • 404 No organization or membership the caller can access under these ids
  • 409 The confirmation is invalid, expired, already used, or its effects changed; the member is already suspended; or this is the last active owner
  • 428 No confirmation was sent; preview the operation first
curl -X POST "https://api.mencoro.com/api/v1/organizations/$MENCORO_ORG_ID/members/$MEMBER_ID/suspend" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "X-Mencoro-Confirmation: $X_MENCORO_CONFIRMATION" \
  -H "Idempotency-Key: $(uuidgen)"

Example response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "userId": "00000000-0000-0000-0000-000000000000",
  "role": "owner",
  "state": "active",
  "isActive": false,
  "joinedAt": "2026-01-01T00:00:00+00:00"
}

Organization operations

POST /api/v1/organization-operation-previews

Preview an organization operation and obtain a confirmation

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.

Request body application/json, Required

Field Type Description
action createOrganization | updateOrganization | archiveOrganization | restoreOrganization | changeMemberRole | suspendMember | reactivateMember | createInvitation | cancelInvitation
organizationId string (uuid) Required for every action except createOrganization.
resourceId string (uuid) The member or invitation the action acts on, for the actions that name one.
payload object The body you intend to send to the operation itself.

Responses

  • 200 What the operation would do, plus a confirmation
  • 400 Unknown action, or a payload the operation does not accept
  • 401 Missing or invalid API key
  • 403 The key lacks a required capability, or its scope is too narrow
  • 404 No organization the caller can access under this id
curl -X POST "https://api.mencoro.com/api/v1/organization-operation-previews" \
  -H "Authorization: Bearer $MENCORO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "action": "createOrganization",
  "organizationId": "00000000-0000-0000-0000-000000000000",
  "resourceId": "00000000-0000-0000-0000-000000000000",
  "payload": {}
}'

Example response

{
  "action": "",
  "organization": {
    "id": "00000000-0000-0000-0000-000000000000",
    "name": ""
  },
  "resourceId": "00000000-0000-0000-0000-000000000000",
  "changes": [
    {
      "code": null,
      "summary": null,
      "targets": null
    }
  ],
  "sideEffects": [
    {
      "code": null,
      "summary": null,
      "targets": null
    }
  ],
  "warnings": [
    {
      "code": null,
      "summary": null,
      "targets": null
    }
  ],
  "conditions": [
    {
      "code": null,
      "summary": null,
      "targets": null
    }
  ],
  "actor": {},
  "confirmation": {}
}

Start tracking your brand in AI search today

Monitor how AI engines cite your brand, track keyword positions, and benchmark against competitors, all in one platform.