---
title: "Conventions"
description: "The rules every endpoint follows, so one resource teaches you all of them."
canonical: https://docs.mentio.dev/conventions
markdown: https://docs.mentio.dev/conventions.mdx
---

# Conventions

The rules every endpoint follows, so one resource teaches you all of them.

The API lives under `/v1`. Everything below holds for every resource in the reference, so the shape of one endpoint tells you the shape of the rest.

## Resources and paths

Plural nouns, one id form: `/v1/mentions/{id}`, `/v1/people/{id}`, `/v1/segments/{id}`, `/v1/keywords/{id}`, `/v1/alerts/{id}`, `/v1/channels/{id}`, `/v1/api-keys/{id}`. Ids are prefixed strings (`mm_`, `aut_`, `seg_`, `kw_`, `feed_`, `dest_`, `key_`) and only resolve inside your workspace: an id from another workspace is a 404, never a 403.

Actions that are not plain updates are `POST` verbs on the resource: `/v1/people/{id}/merge`, `/v1/people/{id}/split`, `/v1/alerts/{id}/test`, `/v1/alerts/{id}/run`, `/v1/channels/{id}/test`, `/v1/channels/{id}/rotate-secret`.

Analytics are named reports, not a query language: `/v1/analytics/summary`, `/v1/analytics/series`, `/v1/analytics/breakdown` and `/v1/analytics/share-of-voice`. They share one window grammar (`range` or `from` and `to`, `keywordIds`, `platforms`, `timezone`, `compare`), each returns a typed shape with a `window`, and `series` and `breakdown` take a `by` dimension. Exports are `GET .../export.csv` on the resource they list (`/v1/mentions/export.csv`, `/v1/people/export.csv`) and take the same filters as the list; mentions also export as JSON (`/v1/mentions/export.json`, the list's own objects).

## Methods and status codes

| Method                 | Meaning                                                             | Success                       |
| ---------------------- | ------------------------------------------------------------------- | ----------------------------- |
| `GET`                  | Read one, or list                                                   | 200                           |
| `POST` on a collection | Create                                                              | 201 with the created resource |
| `PATCH`                | Partial update: omitted fields are untouched, `null` clears a field | 200 with the updated resource |
| `DELETE`               | Remove                                                              | 204, no body                  |
| `POST` action          | Do something                                                        | 200 with the outcome          |

There is one way to change a resource: `PATCH` it. A mention is ignored, closed, assigned, snoozed or annotated through the same `PATCH /v1/mentions/{id}`.

## Envelopes

Single resources come back bare. Lists come back as `data` plus paging:

```json
{ "data": [ ... ], "nextCursor": "eyJ..." }
```

`nextCursor` is `null` on the last page. Pass it back as `cursor` with the same filters and sort. Lists that are aggregated rather than streamed (people) page by `offset` and carry `total` instead:

```json
{ "data": [ ... ], "total": 418 }
```

Small configuration lists (keywords, alerts, channels, segments, API keys) are not paged: `data` holds everything.

## Shapes

Fields are nested by concern. A mention has `post`, `author`, `classification` and `triage`; a person has `reach`, `profile`, `stats` and `annotations`. When a whole concern is absent it is `null` as a whole (`author: null` for an anonymous post, `classification: null` while the classifier has not run), never a scatter of nulls.

Computed numbers live under `stats`; what you or your teammates wrote lives under `triage` or `annotations`. Pipeline internals never appear: a mention has a `status` (`open`, `ignored`, `done`) and booleans `relevant` and `delivered`, not the stages it went through.

Every timestamp is an ISO 8601 string in UTC, for example `"2026-09-03T10:04:44.881Z"`. Fields and filters that take an instant (`since`, `until`, `snoozedUntil`) accept ISO 8601 or epoch milliseconds.

Enumerations are lowercase strings and are listed in each field's description in the reference. Platforms are `bluesky`, `hackernews`, `github`, `stackoverflow`, `devto`, `reddit`, `x`, `youtube`, `news`, `linkedin`, `tiktok`, `instagram`, `appstore`, `googleplay`. A keyword's `platforms` names where its term is searched (`[]` for nowhere), so it never holds the two stores: a keyword collects reviews from its `reviewSources` (see [App store reviews](/platforms/reviews)).

## Query parameters

camelCase names. Booleans are `true` or `false`. A list parameter is repeatable or comma-separated: `?tags=vip,customer` and `?tags=vip&tags=customer` are the same. A parameter you omit imposes no constraint.

A value filter comes in three forms: the singular (`platform=x`), the plural, which keeps any of them (`platforms=x,linkedin`), and the negative, which drops all of them (`notPlatforms=youtube`). The singular and plural combine, so `platform=x&platforms=reddit` reads as either. Different filters always narrow each other: `platforms=x,linkedin&notSentiments=negative` is "X or LinkedIn, nothing negative".

Mentions take `platforms`, `sentiments`, `intents`, `keywordIds`, `tags` and `linkHost`, each with a `not` twin, plus `keywordKinds` and `groupIds` with its twin `notGroupIds` (the keyword's [group](/guides/groups)); people take `platforms`, `tags` and `intents` the same way, plus `keywordKinds` and `neverKeywordKinds`. A mention the classifier has not scored yet still passes a `notSentiments` filter: it is not that sentiment.

### OR across groups: anyOf

Every filter above narrows the others. For OR, mentions take `anyOf`: 1 to 10 groups, each one holding the same conditions a [saved view](/guides/views) holds (lists any-of, `not` lists none-of, all of a group's conditions ANDed). A mention passes when at least one group holds, and every other filter still applies: the whole filter is (other filters) AND (group 1 OR group 2 ...). A group holds no `anyOf` of its own, no window (`since`, `until`) and no paging; an empty group, an empty `anyOf` or a misspelled field is a `400`, never "everything".

On `GET /v1/mentions` and both exports, `anyOf` is URL-encoded JSON. Negative posts on Reddit, or anyone about to buy, both on the last week:

```bash
curl -G https://api.mentio.dev/v1/mentions \
  -H "Authorization: Bearer $MENTIO_API_KEY" \
  --data-urlencode 'since=2026-09-24T00:00:00Z' \
  --data-urlencode 'anyOf=[{"platforms":["reddit"],"sentiments":["negative"]},{"intents":["buy_intent"]}]'
```

A saved view, an alert rule's `filter` and the MCP `search_mentions` tool take the same groups as plain JSON. A rule for relevant mentions in English that are either brand mentions from people with 10,000 followers or more, or 1 and 2 star reviews:

```json
{
  "name": "Worth a look",
  "mode": "instant",
  "filter": {
    "languages": ["en"],
    "anyOf": [
      { "keywordKinds": ["brand"], "minFollowers": 10000 },
      { "ratings": [1, 2] }
    ]
  }
}
```

Each list value is one value of the query D1 runs, and one query takes at most 100. A filter that would need more (long lists in many groups, or a view or alert rule applied on top of yours) answers `400` `filter_too_complex` before anything runs; a view or rule whose groups could never fit is refused when it is saved.

## Errors

One envelope, stable codes, see [Errors](/errors):

```json
{ "error": { "code": "not_found", "message": "Mention not found", "requestId": "3f9c1e2a-..." } }
```

Every response carries an `X-Request-Id` header, and an error repeats it as `error.requestId`; quote it when you write to support. You may send your own `X-Request-Id` (8 to 64 characters of `A-Z`, `a-z`, `0-9`, `.`, `_`, `-`) and it is echoed back, so a request can be traced from your logs to ours; anything else is replaced by an id we mint.

## Keys and scopes

Requests carry `Authorization: Bearer <key>`. A key is `read` or `write`; a `read` key can only call `GET`. Keys belong to one workspace, and every request acts inside it; `GET /v1/whoami` says which one, and whether the credential may write.

## Rate limits

600 requests a minute per workspace, with `X-RateLimit-*` headers on every response and `429 rate_limited` past it. See [Rate limits](/rate-limits).

## Webhooks

Every payload uses one envelope, whatever the event:

```json
{
  "id": "dlv_...",
  "event": "mention.matched",
  "createdAt": "2026-09-03T10:04:44.881Z",
  "alert": { "id": "feed_...", "name": "Buying signals" },
  "data": { ...the resource, shaped exactly as the API returns it... }
}
```

Every request is signed with your channel secret: `X-Mentions-Signature-V2` is `v2=` plus the hex HMAC-SHA256 of `<X-Mentions-Timestamp>.<raw body>`, so a captured request cannot be replayed once its timestamp is old; `X-Mentions-Signature` over the body alone stays for older verifiers. Events, payloads, retries and verification code are in [Webhooks](/webhooks).

## Versioning and deprecation

The version is in the path: `/v1`. Additive changes (a new endpoint, a new optional field or filter, a new value in an enum) ship on `/v1` without notice and never remove or rename anything, so a client that ignores what it does not know keeps working. A change that would break a client ships as a new path (`/v2`); `/v1` keeps working for at least six months after that, every `/v1` response in that window carries `Deprecation` and `Sunset` headers (RFC 9745, RFC 8594) with the date it stops, and this page says what changed. The SDKs, the CLI and the skill are generated from `/v1/openapi.json` and follow it release by release.

## What is not in the reference

The dashboard talks to the same service for sign-in, onboarding, billing, and the Slack and Telegram connection flows. Those endpoints are shaped for the dashboard, change with it, and are deliberately left out of this reference. Everything documented here works with an API key.
