# Overview
What Rumoro does, how a post becomes a mention in your feed, and how to build on it.
Rumoro is social listening for developers and AI agents. You choose the words to watch, such as your product, your competitors or your field. Rumoro finds them on X, Bluesky, Hacker News, Reddit, GitHub, Stack Overflow, DEV, YouTube, LinkedIn, TikTok, Instagram and in the news, and in reviews on the App Store, Google Play, Trustpilot and Google. Each mention is scored for relevance, sentiment and intent. You read them in one feed, triage them, and send them wherever you work.
## From post to feed
1. **Find.** Rumoro checks each [platform](/platforms) on its own schedule and reads Bluesky live. A new keyword also gets recent posts from the last 30 days.
2. **Match.** Each post is compared with every keyword that covers its platform, using the keyword's rules.
3. **Score.** A model reads your company profile and rates the mention. You get relevance from 0 to 100, sentiment, intents like `buy_intent` or `complaint`, and a one-line note.
4. **Send.** Alert rules send relevant mentions right away, or in an hourly, daily or weekly digest, to Slack, Telegram, email or a signed webhook.
You pay for every matched mention, relevant or not. Rules send only relevant ones unless you lower `minRelevance` (0 sends everything). More in [How it works](/how-it-works).
## Ways in
The SDKs, the CLI and the MCP server use the operations in the [OpenAPI document](https://api.rumoro.dev/v1/openapi.json), so fields have the same names everywhere.
## The model
| Resource | What it is | Reference |
| --- | --- | --- |
| Keywords | Terms you track (`brand`, `competitor`, `topic`), with their platforms, matching rules (required and excluded terms, excluded authors, case) and context for the classifier. | [/v1/keywords](/api/keywords/list-keywords) |
| Groups | Keywords grouped by customer, campaign or product. Each group can have its own company description and its own bill. | [/v1/groups](/api/groups/list-groups) |
| Filters | Workspace rules that drop noise, such as excluded terms, authors, GitHub repositories and subreddits. Dropped posts are never stored or billed. | [/v1/filters](/api/filters/get-filters) |
| Mentions | A post matched to a keyword, with its scores, your verdict and triage (`open`, `ignored` or `done`, assignee, snooze, note). | [/v1/mentions](/api/mentions/search-mentions) |
| Views | Saved mention filters. | [/v1/views](/api/views/list-views) |
| People | The authors behind mentions, with reach, profile, tags, notes and mutes. | [/v1/people](/api/people/list-people) |
| Segments | Saved people filters that are always up to date. | [/v1/segments](/api/segments/list-segments) |
| Alerts and channels | Rules that send mentions to Slack, Telegram, email or webhooks, right away or as digests. | [/v1/alerts](/api/alerts/list-alerts) |
| Attention | Hourly checks for spikes, negative turns, noisy keywords and failing channels. | [/v1/attention](/api/attention/list-attention) |
| Analytics | Five reports. Summary, series, breakdown, share of voice and reviews. | [/v1/analytics](/api/analytics/get-analytics-summary) |
| Company | Your description, use cases, competitors and guidelines. The classifier reads it, so it matters most for relevance. | [/v1/company](/api/company/get-company) |
| Members | Your team, roles and invitations. Any credential can read them. Changes need a signed-in owner or admin, in the dashboard or through OAuth. API keys can't change the team. | [/v1/members](/api/members/list-members) |
| Usage and billing | Balance, daily cost, billed keywords and matches. | [/v1/usage](/api/usage/get-usage) |
| API keys | `read` or `write` keys for one workspace, with an optional expiry. `GET /v1/whoami` tells you which key is calling. | [/v1/api-keys](/api/api-keys/list-api-keys) |
## Start here
- [Quickstart](/quickstart) creates a keyword, sets your company profile, searches and triages, in five calls.
- [Authentication](/authentication) and [Conventions](/conventions) cover keys, scopes, response shapes, ids and errors.
- [Alerts](/alerts) sends mentions to you. [Billing](/billing) explains the prepaid balance.
- Building with an agent? Connect the [MCP server](/mcp) or give it [llms.txt](/llms.txt).
# Quickstart
Create a keyword, describe your company, find mentions and mark one done. Five API calls.
Rumoro finds your keywords on X, Bluesky, Hacker News, Reddit, GitHub, Stack Overflow, DEV, YouTube, LinkedIn, TikTok, Instagram, in the news and in reviews, and scores each mention. Each example comes in TypeScript, Python and curl. The [CLI](/cli) and [MCP server](/mcp) can do the same.
## Before you start
Sign up at [app.rumoro.dev](https://app.rumoro.dev). You get $5.80 of credit and don't need a card. Create a key on the **API keys** page and copy it, because it is shown only once. The API is at `https://api.rumoro.dev/v1`. Every request needs the key, except `/v1/health` and `/v1/openapi.json`. The examples read it from `RUMORO_API_KEY`.
```ts tab="TypeScript"
// npm install @rumoro-dev/sdk
import { createRumoro } from '@rumoro-dev/sdk';
const rumoro = createRumoro({ apiKey: process.env.RUMORO_API_KEY! });
```
```python tab="Python"
# pip install rumoro
import os
from rumoro import Rumoro
rumoro = Rumoro(api_key=os.environ["RUMORO_API_KEY"])
```
```bash tab="curl"
export RUMORO_API_KEY="ref_..."
```
More in [Authentication](/authentication).
## 1. Add a keyword
```ts tab="TypeScript"
const { data: keyword } = await rumoro.createKeyword({ body: { term: 'driftwood', kind: 'brand' } });
```
```python tab="Python"
keyword = rumoro.keywords.create(term="driftwood", kind="brand")
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/keywords \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "term": "driftwood", "kind": "brand" }'
```
```json
{
"id": "kw_3b8f2c71d94e4a06a5e19c2f7d6b0e48",
"term": "driftwood",
"kind": "brand",
"muted": false,
"platforms": null,
"context": null,
"matching": { "requiredTerms": [], "requiredMode": "any", "excludedTerms": [], "excludedAuthors": [], "caseSensitive": false },
"createdAt": "2026-10-04T08:21:37.512Z"
}
```
This response is shortened. `kind` can be `brand` (the default), `competitor` or `topic`. `platforms: null` searches every platform. If your term is a common word, add `matching` rules, such as words the post must also contain or words that rule it out, and a `context` sentence. See [How it works](/how-it-works#matching-rules). If your balance can't pay for another day of the keyword, you get `402 insufficient_balance` ([Billing](/billing)).
## 2. Describe your company
Rumoro scores relevance against your company profile, so this step improves results more than any other.
```ts tab="TypeScript"
await rumoro.updateCompany({
body: { name: 'Driftwood', description: 'A feature flag service for mobile teams.', useCases: ['Roll out a release to 5% of users'] },
});
```
```python tab="Python"
rumoro.company.update(
name="Driftwood", description="A feature flag service for mobile teams.", useCases=["Roll out a release to 5% of users"]
)
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/company \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Driftwood", "description": "A feature flag service for mobile teams.", "useCases": ["Roll out a release to 5% of users"] }'
```
The classifier reads a `context` that Rumoro builds from the profile. To write that text yourself, send `context`.
## 3. Find mentions
Mentions appear as each platform is checked.
```ts tab="TypeScript"
const { data: page } = await rumoro.searchMentions({ query: { minRelevance: 60, sentiment: 'negative', limit: 20 } });
```
```python tab="Python"
page = rumoro.mentions.search(min_relevance=60, sentiment="negative", limit=20)
```
```bash tab="curl"
curl "https://api.rumoro.dev/v1/mentions?minRelevance=60&sentiment=negative&limit=20" \
-H "Authorization: Bearer $RUMORO_API_KEY"
```
```json
{
"data": [
{
"id": "mm_...",
"status": "open",
"relevant": true,
"keyword": { "id": "kw_...", "term": "driftwood" },
"post": { "platform": "reddit", "url": "https://www.reddit.com/r/...", "text": "..." },
"author": { "name": "...", "followers": null },
"classification": { "relevance": 78, "sentiment": "negative", "intents": ["bug_report"], "note": "..." },
"createdAt": "2026-10-04T07:55:10.000Z"
}
],
"nextCursor": null
}
```
This response is shortened. The newest mentions come first. To get the next page, send `nextCursor` as `cursor`, and stop when it is `null`. See [Conventions](/conventions).
## 4. Mark one done
Mentions marked done or ignored are not sent in new alerts.
```ts tab="TypeScript"
await rumoro.updateMention({ path: { id: 'mm_...' }, body: { status: 'done', note: 'Replied in the thread' } });
```
```python tab="Python"
rumoro.mentions.update("mm_...", status="done", note="Replied in the thread")
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/mentions/mm_... \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "done", "note": "Replied in the thread" }'
```
## 5. Get alerts
Instead of polling, have mentions sent to you. Use a [webhook](/webhooks) for your own code, or [Slack](/alerts/slack), [Telegram](/alerts/telegram) and email through [Alerts](/alerts).
## Next steps
# Authentication
How to authenticate with an API key or OAuth, what read and write scopes allow, and how to manage keys.
Each request carries a Bearer credential for one workspace, an **API key** or an **OAuth token** from an MCP sign-in. Both work on REST and the [MCP server](/mcp).
```ts tab="TypeScript"
import { createRumoro } from '@rumoro-dev/sdk';
const rumoro = createRumoro({ apiKey: process.env.RUMORO_API_KEY! });
await rumoro.listKeywords();
```
```python tab="Python"
import os
from rumoro import Rumoro
rumoro = Rumoro(api_key=os.environ["RUMORO_API_KEY"])
rumoro.keywords.list()
```
```bash tab="curl"
curl https://api.rumoro.dev/v1/keywords -H "Authorization: Bearer $RUMORO_API_KEY"
```
`GET /v1/health`, `GET /v1/openapi.json` and MCP's tool list need no credential. MCP tool calls do.
`GET /v1/whoami` shows the workspace, the credential type (`auth.kind`), its `scope`, and a key's id and expiry. Call it first to catch a wrong workspace or read-only key.
```json
{
"workspace": { "id": "org_...", "name": "Driftwood" },
"auth": { "kind": "api_key", "scope": "write", "apiKeyId": "key_...", "expiresAt": null },
"user": null
}
```
For OAuth, `user` is the person.
## API keys
- Keys start with `ref_`, belong to one workspace and are shown once. Only a SHA-256 hash is stored.
- `expiresAt` (ISO 8601 or epoch milliseconds) turns a key off at that time. It stays listed until revoked.
- A workspace can have 100 keys that aren't revoked (`409 key_limit`).
## Scopes
`scope` is `read` or `write` (the default). A `read` key can only `GET`, otherwise it gets `403 read_only_key`. Over MCP it sees only read tools.
## OAuth sign-in
MCP clients with OAuth only need `https://mcp.rumoro.dev/mcp`. The person signs in, picks a workspace and allows read or write access.
- Access tokens last an hour, refresh tokens 30 days.
- A token acts as the person, within their role. Owners and admins manage the team, and only owners manage keys.
- Tokens and keys share the workspace [rate limit](/rate-limits).
## Managing keys
| Method | Path | What it does |
| --- | --- | --- |
| `POST` | `/v1/api-keys` | Creates a key, shown only here |
| `GET` | `/v1/api-keys` | Lists keys without secrets |
| `DELETE` | `/v1/api-keys/{id}` | Revokes a key |
```ts tab="TypeScript"
const { data: key } = await rumoro.createApiKey({
body: { name: 'nightly-report', scope: 'read', expiresAt: '2027-01-31T00:00:00Z' },
});
```
```python tab="Python"
key = rumoro.api_keys.create(name="nightly-report", scope="read", expiresAt="2027-01-31T00:00:00Z")
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/api-keys \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "nightly-report", "scope": "read", "expiresAt": "2027-01-31T00:00:00Z" }'
```
```json
{ "id": "key_...", "name": "nightly-report", "prefix": "ref_5d1e09a2", "scope": "read", "createdAt": "2026-10-04T08:30:12.104Z", "lastUsedAt": null, "expiresAt": "2027-01-31T00:00:00.000Z", "key": "ref_5d1e09a2-..." }
```
`lastUsedAt` updates at most once a minute. See the [reference](/api/api-keys/create-api-key).
## When authentication fails
A missing or invalid credential gets `401` with the [error envelope](/errors). Its `WWW-Authenticate` header sends OAuth clients to sign-in.
```json
{ "error": { "code": "unauthorized", "message": "Invalid API key or access token", "requestId": "..." } }
```
Without a credential the message is "Missing or invalid credentials". MCP answers `{ "error": "" }`.
# How it works
How Rumoro collects posts, how often it checks each platform, which rules filter them, and what happens to a mention.
## From post to mention
Rumoro checks each platform on its own schedule. Every post is brought into one common shape, compared with the keywords of every workspace, scored by a language model, and sent on by your alert rules.
Workspaces share the collection. If two workspaces track the same term, Rumoro asks the platform once and stores the post once, then matches it for both. Hacker News is the exception, because it is searched for each keyword every 5 minutes. Your feed shows the part of this collection that matches your keywords.
## How often platforms are checked
Some platforms are read live (Bluesky), others once a day (reviews). The [Platforms](/platforms) tab lists each one.
On X, LinkedIn, Reddit, TikTok, Instagram and GitHub, a keyword that keeps finding nothing is checked less often. Its next match brings it back to the normal schedule. `polling` on a keyword shows when each platform was last checked and how many checks in a row found nothing.
A new keyword starts with the 10 newest matching posts from the last 30 days on each platform that supports it (10 days on Instagram). After that it only gets new posts. If you add a platform to an existing keyword, that platform starts from that moment, without a look-back. Look-back mentions are billed. They appear in the feed and in digests, but not in instant alerts.
## Matching rules
A term matches as a phrase, ignoring case. Two sets of rules remove posts before they are stored. A post they remove is never scored or billed, which is what makes them different from an alert's filter.
| Level | Where | Rules |
| --- | --- | --- |
| Keyword | `matching` on [`POST`](/api/keywords/create-keyword) and [`PATCH /v1/keywords/{id}`](/api/keywords/update-keyword) | `requiredTerms`, with `requiredMode` set to `any` or `all`. `excludedTerms`, where `*` at the start or end is a wildcard (`promo*`). `excludedAuthors`, written like an alert's mute list. `caseSensitive`, for acronyms such as `RAG` that shouldn't match `rag`. |
| Workspace | [`GET`](/api/filters/get-filters) and [`PATCH /v1/filters`](/api/filters/update-filters) | `excludedTerms` and `excludedAuthors` for all keywords, `excludedRepos` for GitHub repositories (`owner/name`), and `subreddits.only` or `subreddits.excluded`. |
A keyword can also have a `context` sentence that helps the classifier with that term, for example "Mercury is our banking app, not the planet or the metal." It changes the score, not what is stored. Changed rules apply from the next check. Mentions you already have stay.
## Mention status
A mention's state is in four fields.
| Field | Values | Meaning |
| --- | --- | --- |
| `status` | `open`, `ignored`, `done` | Your triage, set with [`PATCH /v1/mentions/{id}`](/api/mentions/update-mention). New mentions are `open`. |
| `relevant` | `true`, `false` | Relevance is 40 or higher. Stays `false` until the mention is scored. |
| `delivered` | `true`, `false` | At least one alert channel received it. |
| `classification` | object or `null` | `null` until scored. `failed: true` when scoring failed. |
All matches stay searchable. Noise (`relevant: false`) is only sent when a rule's `minRelevance` is below 40, and `GET /v1/mentions?relevant=false` lists it. Mentions marked ignored or done are not sent in new alerts.
## Scores
The classifier reads your [company profile](/api/company/get-company), or the description of the keyword's [group](/guides/groups) if it has one, and fills in `classification`.
| Field | Values |
| --- | --- |
| `relevance` | 0 to 100 |
| `sentiment` | `positive`, `neutral`, `negative` |
| `intents` | `buy_intent`, `question`, `complaint`, `praise`, `comparison`, `churn_intent`, `bug_report`, `pricing`, `hiring`, `event`, `promotional`, `testimonial` (someone recommends it from experience), `industry_insight` (facts or trends about the market), `launch` (something new is announced), `feedback` (an idea or a request) |
| `automated` | `true` for bots, templates and generated digests |
| `language` | An ISO 639-1 code such as `en` or `es`, or `null` |
| `confidence` | How sure the relevance score is, from 0 to 1, or `null` |
| `uncertain` | `true` when confidence is below 0.4 or the model that wrote the note disagreed with the score. It marks the mention but filters nothing. |
| `note` | Why the mention matters, in up to 200 characters |
| `failed` | `true` when scoring failed. Failed mentions are never billed. |
| `feedback` | Your verdict, if you gave one, and the values it replaced |
### Your verdict
Correct a score with [`PATCH /v1/mentions/{id}`](/api/mentions/update-mention).
- `relevant: false` marks a mention as noise. Its relevance becomes 0, and it drops out of the relevant feed, digests, counts and analytics.
- `relevant: true` rescues a mention the classifier missed and sets its relevance to 100.
- `sentiment` fixes the sentiment label.
- `null` undoes your verdict and restores the classifier's values from `classification.feedback`.
Verdicts don't change what you pay. Each keyword counts them in `stats.feedback`. A mention that isn't scored yet returns `409 classification_pending`.
Good scores need good context. Use `PATCH /v1/company` to describe your company, its use cases, its competitors and what counts as relevant to you. A keyword's `context` adds detail for one term.
## Author data
Each mention keeps the public post and its author's public name, handle, picture and follower count. On some platforms Rumoro also reads public profile details such as bio, company, location, website and linked accounts. It keeps an email only when the person published one there. Every workspace that matched the person shares these details.
Rumoro relies on legitimate interests (GDPR Art. 6(1)(f)) to process this data. Your workspace's notes, tags and outreach about a person belong to you, and you are their controller. The [MCP tools](/mcp) never return an email address, so `profile.email` is always `null` there. The REST API and the dashboard show it where the person published it.
Anyone can ask to have their accounts removed at [rumoro.dev/privacy/remove](https://rumoro.dev/privacy/remove). We finish each request within 30 days. Their mentions then leave every workspace, with the notes, tags and outreach attached to them, and Rumoro stops storing their new posts. Charges for those mentions stay in your usage and ledger.
# SDKs
Official TypeScript and Python clients for the Rumoro API, or generate one from the OpenAPI document.
Rumoro has official clients for TypeScript and Python. Both are generated from the [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) that the [API reference](/api) is built from. Each endpoint is one call with the same name, and requests and responses are typed. You need an [API key](/authentication).
| Language | Package | Install |
| --- | --- | --- |
| TypeScript | [`@rumoro-dev/sdk`](https://www.npmjs.com/package/@rumoro-dev/sdk) | `npm install @rumoro-dev/sdk` |
| Python | [`rumoro`](https://pypi.org/project/rumoro/) | `pip install rumoro` |
The [TypeScript](/sdks/typescript) and [Python](/sdks/python) guides cover the first call, errors, paging and async use.
## Other languages
For Go, Java, Ruby, PHP, .NET, Rust or anything else, you have three options.
1. **Use the [CLI](/cli).** Each endpoint is a `rumoro` command that prints JSON. Any language that can start a process can call it, for example `rumoro mentions:search --relevant true | your-script`.
2. **Call the API directly.** It uses a Bearer key, JSON requests and responses, one error format and camelCase field names. [Conventions](/conventions) explains it all on one page.
3. **Generate your own client.** Run [OpenAPI Generator](https://openapi-generator.tech) or [Kiota](https://learn.microsoft.com/openapi/kiota/) on `https://api.rumoro.dev/v1/openapi.json`. Each operation id becomes a method, and each tag a group.
## Versions
When the API changes, both clients and the CLI are generated again from the OpenAPI document and released together. The [changelog](/changelog) says what changed.
# TypeScript SDK
@rumoro-dev/sdk on npm. One typed function for each Rumoro API operation.
`@rumoro-dev/sdk` gives you one typed function for each endpoint, with the names from the [API reference](/api), such as `searchMentions`, `createKeyword` or `getAnalyticsSummary`. It is generated from the OpenAPI document, runs on Node 22 or newer and has no runtime dependencies. Use it on a server. The API doesn't accept requests from browsers on other sites, which keeps your [API key](/authentication) out of web pages.
```bash
npm install @rumoro-dev/sdk
```
## First call
```ts
import { createRumoro } from '@rumoro-dev/sdk';
const rumoro = createRumoro({ apiKey: process.env.RUMORO_API_KEY! });
const { data, error } = await rumoro.searchMentions({
query: { platform: 'reddit', relevant: true, limit: 20 },
});
if (error) throw new Error(`${error.error.code}: ${error.error.message}`);
for (const mention of data.data) {
console.log(mention.post.platform, mention.classification?.relevance, mention.post.url);
}
```
A call never throws on an HTTP error. It returns `{ data, error, request, response }`, and on failure `error` holds the API's [error envelope](/errors) while `data` is undefined. To get `data` directly and an exception on failure, pass `throwOnError: true`.
```ts
const { data: keyword } = await rumoro.createKeyword({
body: { term: 'driftwood', kind: 'brand', platforms: ['reddit', 'github'] },
throwOnError: true,
});
```
## Calls
Put ids from the path in `path`, query parameters in `query` and the request body in `body`.
```ts
await rumoro.updateMention({ path: { id: 'mm_...' }, body: { status: 'done', note: 'Answered in the thread' } });
await rumoro.getPerson({ path: { id: 'aut_...' } });
await rumoro.createAlert({
body: { name: 'Pricing questions', mode: 'instant', event: 'mention.pricing', filter: { intents: ['pricing'] }, channelIds: ['dest_...'] },
});
await rumoro.getAnalyticsSummary({ query: { range: '7d', compare: true, timezone: 'America/New_York' } });
```
For the next page, send `nextCursor` as `cursor`. Stop when it is `null`.
```ts
let cursor: string | undefined;
do {
const { data } = await rumoro.searchMentions({ query: { sentiment: 'negative', limit: 100, cursor }, throwOnError: true });
for (const mention of data.data) console.log(mention.id, mention.post.url);
cursor = data.nextCursor ?? undefined;
} while (cursor);
```
A CSV export returns a string.
```ts
const { data: csv } = await rumoro.exportMentionsCsv({ query: { since: '2026-10-01T00:00:00Z' }, throwOnError: true });
```
## Types
All schemas are exported, such as `Mention`, `Keyword`, `Person`, `Segment`, `Alert`, `Channel`, `Company`, `AnalyticsSummary` and `ShareOfVoice`. Every call also has an `Data` and an `Response` type.
```ts
import type { Mention, SearchMentionsData } from '@rumoro-dev/sdk';
type SearchQuery = NonNullable;
const recent: SearchQuery = { relevant: true, limit: 50 };
```
## Configuration
```ts
const rumoro = createRumoro({
apiKey: 'ref_...',
baseUrl: 'https://api.rumoro.dev', // the default
fetch: customFetch, // for tests, or a runtime without fetch
headers: { 'x-request-source': 'crm-sync' }, // added to every request
});
```
`rumoro.client` is the client underneath. Use `rumoro.client.interceptors` to change requests or responses, and `rumoro.client.request(...)` to send any request yourself.
# Python SDK
rumoro on PyPI. Every Rumoro API operation from Python, sync and async.
The `rumoro` package has one method per endpoint, grouped by resource, and a typed model for every response. It is built from the OpenAPI document behind the [API reference](/api). It needs Python 3.11 or newer and works sync and async.
```bash
pip install rumoro
```
## First call
```python
from rumoro import Rumoro
rumoro = Rumoro(api_key="ref_...")
page = rumoro.mentions.search(platform="reddit", relevant=True, limit=20)
for mention in page.data:
score = mention.classification.relevance if mention.classification else None
print(mention.post.platform.value, score, mention.post.url)
```
A call returns the endpoint's model, a `str` for CSV exports, or `None` for a 204. An error raises `RumoroError` with `status`, `code` and `message`.
```python
from rumoro import RumoroError
try:
rumoro.keywords.create(term="a", kind="brand")
except RumoroError as err:
print(err.status, err.code, err.message) # 400 validation_error term: ...
```
If the body is not an API error, such as a proxy's HTML page, `code` is `http_`.
## Calls
- Ids are positional.
- Query parameters are keyword arguments in snake_case, such as `keyword_ids`. `range` and `from` become `range_` and `from_`.
- A body can be keyword arguments, a dict or a model, with the API's field names such as `channelIds`.
- Enum values can be plain strings.
- Time parameters (`since`, `until`, `snoozed_until`) work with `datetime` and `date` objects, ISO 8601 strings and epoch milliseconds.
```python
rumoro.keywords.create(term="driftwood", kind="brand", platforms=["reddit", "github"])
rumoro.keywords.update("kw_...", muted=True)
rumoro.mentions.update("mm_...", status="done", note="Answered in the thread")
rumoro.people.merge("aut_...", into="aut_...")
rumoro.alerts.create(name="Pricing questions", mode="instant", filter={"intents": ["pricing"]}, channelIds=["dest_..."])
rumoro.analytics.summary(range_="7d", compare=True, timezone="America/New_York")
```
For the next page, send `next_cursor` as `cursor`, and stop when it is `None`.
```python
cursor = None
while True:
page = rumoro.mentions.search(sentiment="negative", limit=100, cursor=cursor)
for mention in page.data:
print(mention.id, mention.post.url)
cursor = page.next_cursor
if not cursor:
break
```
A CSV export returns a string.
```python
csv_text = rumoro.mentions.export(since="2026-10-01T00:00:00Z")
with open("mentions.csv", "w", encoding="utf-8") as file:
file.write(csv_text)
```
## Async
`AsyncRumoro` has the same methods, as coroutines.
```python
import asyncio
from rumoro import AsyncRumoro
async def main() -> None:
async with AsyncRumoro(api_key="ref_...") as rumoro:
summary = await rumoro.analytics.summary(range_="30d")
print(summary.matched, summary.relevant)
asyncio.run(main())
```
## Resources
| Attribute | Methods |
| --- | --- |
| `keywords` | `create`, `list`, `get`, `update`, `delete`, `health` |
| `groups` | `create`, `list`, `get`, `update`, `delete` |
| `mentions` | `search`, `get`, `update`, `export`, `export_json` |
| `attention` | `list`, `dismiss` |
| `views` | `create`, `list`, `get`, `update`, `delete` |
| `filters` | `get`, `update` |
| `people` | `list`, `get`, `update`, `merge`, `split`, `export`, `activities`, `log_activity`, `delete_activity` |
| `segments` | `create`, `list`, `get`, `update`, `delete` |
| `alerts` | `create`, `list`, `get`, `update`, `delete`, `test`, `run`, `mute`, `unmute` |
| `channels` | `create`, `list`, `get`, `update`, `delete`, `test`, `rotate_secret`, `deliveries` |
| `analytics` | `summary`, `series`, `breakdown`, `share_of_voice`, `reviews` |
| `company` | `get`, `update` |
| `members` | `list`, `remove`, `invitations`, `invite`, `revoke_invitation` |
| `usage` | `get`, `breakdown` |
| `billing` | `wallet`, `ledger`, `top_up`, `invoices`, `invoice_url` |
| `api_keys` | `create`, `list`, `revoke` |
| `auth` | `whoami` |
| `system` | `health` |
## Lower level
`rumoro.client` is the generated `httpx`-based client with your key. Each `rumoro.api` module is one operation, and its `sync_detailed` returns the full response.
```python
from rumoro.api.mentions import search_mentions
rumoro = Rumoro("ref_...", base_url="https://api.rumoro.dev", timeout=10.0, headers={"x-request-source": "crm-sync"})
response = search_mentions.sync_detailed(client=rumoro.client, limit=10)
```
# CLI
@rumoro-dev/cli on npm. The Rumoro API in your terminal, with browser sign-in, a live mention feed and MCP setup.
`rumoro` puts the Rumoro API in your terminal. Each endpoint is a `noun:verb` command, generated from the OpenAPI document. Three more commands, `auth:*`, `mentions:watch` and `mcp:config`, do what one request can't. It needs Node 22 or newer.
```bash
npm install -g @rumoro-dev/cli
rumoro --help
```
You can also run it without installing, as in `npx @rumoro-dev/cli mentions:search --relevant true`.
## Sign in
```bash
rumoro auth:login
```
This opens the dashboard in your browser. A workspace owner approves once. The key, named after your computer, reaches the CLI over a local port only and is saved in `~/.rumoro/config.json` (mode 600). Revoke it any time on the **API keys** page.
- `--scope read` asks for a read-only key.
- `--no-open` prints the address instead. On a remote machine, approve in any browser and run the `rumoro auth:set --key …` command the page shows.
- Members who aren't owners save a key an owner created, with `auth:set`.
There are three other ways to give the CLI a key. The first one wins.
```bash
rumoro keywords:list --api-key ref_... # for one command
export RUMORO_API_KEY=ref_... # in the environment, for CI
rumoro auth:set --key ref_... # saved on this computer
```
`auth:check` shows the key's workspace and source, and `auth:logout` deletes it. To use another host, set `--api-url`, `RUMORO_API_URL` or `auth:set --url`. A key from `rumoro api-keys:create --name reports --scope read` can only run read commands.
## Commands
Each area has a page with every command and flag. See [keywords](/cli/keywords), [mentions](/cli/mentions), [people and segments](/cli/people), [alerts and channels](/cli/alerts), [analytics](/cli/analytics) and [account](/cli/account).
```bash
rumoro keywords:create --term "driftwood" --kind brand --platforms reddit,github
rumoro keywords:update kw_... --muted true
rumoro mentions:search --sentiment negative --limit 20
rumoro mentions:update mm_... --status done --note "Answered in the thread"
rumoro people:list --minFollowers 10000 --sort reach
rumoro analytics:summary --range 7d
rumoro alerts:create --name "Pricing questions" --mode instant --filter '{"intents":["pricing"]}' --channelIds dest_...
```
- Ids are positional. Everything else is a flag with the API's field name.
- Every flag takes a value, including booleans (`--muted false`).
- Lists are comma-separated, objects are JSON, and `null` clears a field that allows it (`--note null`).
- `--json '{...}'` sends a whole body, and `--json -` reads it from stdin. Flags you add override fields in it.
## Output
Output is JSON, compact when piped and indented on a terminal or with `--pretty`. `--table` prints lists in columns.
```bash
rumoro keywords:list --table
rumoro mentions:search --limit 5 | jq '.data[].post.url'
rumoro mentions:export --platform reddit --out reddit.csv
```
API errors go to stderr as the [error envelope](/errors) and exit with 1. So does an unknown option or command. An invalid value or a missing required flag exits with 2 and a `usage` error, and a missing key with 2 and `no_api_key`. CSV exports print to stdout, or go to a file with `--out`.
## Follow the feed
```bash
rumoro mentions:watch --platform github --relevant true --interval 30
```
`mentions:watch` checks the newest mentions again and again and prints each new one as a line of JSON, oldest first. It takes the filters of `mentions:search` except `--since`, `--until`, `--sort`, `--cursor` and `--limit`. `--from-start` prints the current page first.
## MCP setup
To connect an AI client to the [MCP server](/mcp), run `mcp:config`. It includes your key.
```bash
rumoro mcp:config # a claude mcp add command
rumoro mcp:config --client cursor # JSON for .cursor/mcp.json
rumoro mcp:config --client vscode # JSON for .vscode/mcp.json
```
## Restish
[Restish](https://rest.sh) is a general-purpose API command line. It can read the same OpenAPI document.
```bash
restish api configure rumoro https://api.rumoro.dev/v1/openapi.json
```
# Account
Manage keys, billing, the team and sign-in, set up MCP and check the API.
Pass ids as arguments and everything else as flags with the API's field names. The [API reference](/api) explains each field, and `--help` on any command lists its flags. Every command also takes `--api-key`, `--api-url`, `--pretty` and `--table`.
```bash
rumoro auth:login
rumoro api-keys:create --name nightly-sync --scope read --expiresAt 2027-01-31T00:00:00Z
rumoro company:update --description "Driftwood ships a preview deploy for every pull request" --competitors Vercel,Netlify
rumoro billing:ledger --limit 20 --table
rumoro billing:top-up --amountCents 2000
rumoro members:invite --email sam@example.com --role admin
rumoro mcp:config --client vscode
rumoro system:health
```
| Command | What it does | Flags |
| --- | --- | --- |
| `api-keys:create` | Create an API key | `--name`, `--scope`, `--expiresAt`, `--json` |
| `api-keys:list` | List API keys | |
| `api-keys:revoke ` | Revoke an API key | |
| `auth:check` | Show the key's workspace and its source | |
| `auth:login` | Sign in through the browser; a workspace owner approves and the key is stored | `--scope`, `--name`, `--no-open`, `--app-url`, `--timeout` |
| `auth:logout` | Delete the saved key | |
| `auth:set` | Save a key, and an API host if given, to ~/.rumoro/config.json | `--key`, `--url` |
| `auth:whoami` | Check the credential | |
| `billing:invoice-url ` | Get a receipt link | |
| `billing:invoices` | List receipts | |
| `billing:ledger` | List ledger entries | `--cursor`, `--limit` |
| `billing:top-up` | Start a top-up | `--amountCents`, `--successUrl`, `--json` |
| `billing:wallet` | Get the balance | |
| `company:get` | Get the company profile | |
| `company:update` | Update the company profile | `--name`, `--description`, `--useCases`, `--accounts`, `--website`, `--competitors`, `--guidelines`, `--context`, `--json` |
| `mcp:config` | Print MCP settings for a client, with your key in them | `--client` (claude, cursor, vscode, generic), `--url` |
| `members:invitations` | List pending invitations | |
| `members:invite` | Invite a member | `--email`, `--role`, `--json` |
| `members:list` | List members | |
| `members:remove ` | Remove a member | |
| `members:revoke-invitation ` | Revoke an invitation | |
| `system:health` | Check that the API is up (no key needed) | |
| `usage:breakdown` | Get the usage breakdown | `--by`, `--range`, `--month`, `--limit`, `--offset` |
| `usage:get` | Get usage and balance | |
# Alerts and attention
Create alerts and channels, test them, and handle attention items.
Pass ids as arguments and everything else as flags with the API's field names. The [API reference](/api) explains each field, and `--help` on any command lists its flags. Every command also takes `--api-key`, `--api-url`, `--pretty` and `--table`.
```bash
rumoro channels:create --kind webhook --url https://hooks.example.com/rumoro --label "Support bot"
rumoro alerts:create --name "Bug reports" --mode instant --filter '{"intents":["bug_report"]}' --channelIds dest_...
rumoro alerts:create --name "Daily roundup" --mode daily --schedule '{"hour":8,"minute":30,"timezone":"America/New_York"}' --channelIds dest_...
rumoro alerts:test feed_...
rumoro channels:deliveries dest_... --table
rumoro attention:dismiss att_...
```
| Command | What it does | Flags |
| --- | --- | --- |
| `alerts:create` | Create an alert | `--name`, `--enabled`, `--mode`, `--filter`, `--schedule`, `--event`, `--channelIds`, `--json` |
| `alerts:delete ` | Delete an alert | |
| `alerts:get ` | Get an alert | |
| `alerts:list` | List alerts | |
| `alerts:mute ` | Mute authors | `--authors`, `--json` |
| `alerts:run ` | Send a digest now | |
| `alerts:test ` | Test an alert | |
| `alerts:unmute ` | Unmute authors | `--authors`, `--json` |
| `alerts:update ` | Update an alert | `--name`, `--enabled`, `--mode`, `--filter`, `--schedule`, `--event`, `--channelIds`, `--json` |
| `attention:dismiss ` | Dismiss an attention item | |
| `attention:list` | List attention items | `--status`, `--kind`, `--limit`, `--cursor` |
| `channels:create` | Create a channel | `--kind`, `--channelId`, `--channelName`, `--events`, `--emails`, `--url`, `--label`, `--headers`, `--json` |
| `channels:delete ` | Delete a channel | |
| `channels:deliveries ` | List a channel's deliveries | `--limit` |
| `channels:get ` | Get a channel | |
| `channels:list` | List channels | |
| `channels:rotate-secret ` | Rotate the signing secret | |
| `channels:test ` | Test a channel | |
| `channels:update ` | Update a channel | `--label`, `--url`, `--headers`, `--events`, `--json` |
# Analytics
Summary, series, breakdown, share of voice and reviews for one period.
Pass ids as arguments and everything else as flags with the API's field names. The [API reference](/api) explains each field, and `--help` on any command lists its flags. Every command also takes `--api-key`, `--api-url`, `--pretty` and `--table`.
```bash
rumoro analytics:summary --range 30d --compare true --timezone Europe/Berlin
rumoro analytics:series --range 7d --bucket day --by platform
rumoro analytics:breakdown --range 30d --by keyword --table
rumoro analytics:share-of-voice --range 90d
```
| Command | What it does | Flags |
| --- | --- | --- |
| `analytics:breakdown` | Get a mention breakdown | `--range`, `--from`, `--to`, `--keywordIds`, `--platforms`, `--compare`, `--timezone`, `--by` |
| `analytics:reviews` | Get review stats | `--range`, `--from`, `--to`, `--keywordIds`, `--platforms`, `--compare`, `--timezone`, `--bucket` |
| `analytics:series` | Mentions over time | `--range`, `--from`, `--to`, `--keywordIds`, `--platforms`, `--compare`, `--timezone`, `--bucket`, `--by` |
| `analytics:share-of-voice` | Get share of voice | `--range`, `--from`, `--to`, `--keywordIds`, `--platforms`, `--compare`, `--timezone` |
| `analytics:summary` | Get summary counts | `--range`, `--from`, `--to`, `--keywordIds`, `--platforms`, `--compare`, `--timezone` |
# Keywords and groups
Create and adjust keywords, organize them in groups, and set workspace filters.
Pass ids as arguments and everything else as flags with the API's field names. The [API reference](/api) explains each field, and `--help` on any command lists its flags. Every command also takes `--api-key`, `--api-url`, `--pretty` and `--table`.
```bash
rumoro keywords:create --term "driftwood" --kind brand --platforms github,hackernews,x
rumoro keywords:update kw_... --matching '{"excludedTerms":["beach","furniture"]}'
rumoro keywords:health kw_... --range 30d
rumoro groups:create --name "Northwind" --externalId client_42
rumoro filters:update --excludedRepos driftwood-dev/website
```
| Command | What it does | Flags |
| --- | --- | --- |
| `filters:get` | Get the workspace filters | |
| `filters:update` | Update the workspace filters | `--excludedTerms`, `--excludedAuthors`, `--excludedRepos`, `--subreddits`, `--json` |
| `groups:create` | Create a group | `--name`, `--externalId`, `--context`, `--json` |
| `groups:delete ` | Delete a group | |
| `groups:get ` | Get a group | |
| `groups:list` | List groups | `--externalId` |
| `groups:update ` | Update a group | `--name`, `--externalId`, `--context`, `--json` |
| `keywords:create` | Create a keyword | `--term`, `--kind`, `--platforms`, `--context`, `--matching`, `--cap`, `--groupId`, `--reviewSources`, `--json` |
| `keywords:delete ` | Delete a keyword | |
| `keywords:get ` | Get a keyword | |
| `keywords:health ` | Check a keyword's health | `--range`, `--ai` |
| `keywords:list` | List keywords | `--q`, `--groupId`, `--kind`, `--status`, `--platform`, `--sort`, `--limit`, `--offset` |
| `keywords:update ` | Update a keyword | `--kind`, `--muted`, `--platforms`, `--context`, `--matching`, `--cap`, `--groupId`, `--reviewSources`, `--json` |
# Mentions and views
Find, handle and export mentions, watch new ones arrive, and save views.
Pass ids as arguments and everything else as flags with the API's field names. The [API reference](/api) explains each field, and `--help` on any command lists its flags. Every command also takes `--api-key`, `--api-url`, `--pretty` and `--table`.
```bash
rumoro mentions:search --relevant true --intent buy_intent --limit 20 --table
rumoro mentions:update mm_... --status done --note "replied"
rumoro mentions:export --since 2026-10-01T00:00:00Z --out mentions.csv
rumoro mentions:watch --platform hackernews --intent question
rumoro views:create --name "Bug reports on GitHub" --filter '{"platforms":["github"],"intents":["bug_report"]}'
```
| Command | What it does | Flags |
| --- | --- | --- |
| `mentions:export` | Export mentions as CSV | `--keywordId`, `--platform`, `--status`, `--relevant`, `--sentiment`, `--intent`, `--automated`, `--personId`, `--includeMuted`, `--assigneeId`, `--snoozed`, `--excludeAuthors`, `--minRelevance`, `--minConfidence`, `--minFollowers`, `--maxFollowers`, `--isReply`, `--alertId`, `--viewId`, `--keywordKinds`, `--tags`, `--linkHosts`, `--platforms`, `--notPlatforms`, `--keywordIds`, `--groupIds`, `--notGroupIds`, `--notKeywordIds`, `--sentiments`, `--notSentiments`, `--intents`, `--notIntents`, `--notLinkHosts`, `--notTags`, `--languages`, `--notLanguages`, `--ratings`, `--notRatings`, `--minLikes`, `--minReposts`, `--minReplies`, `--minQuotes`, `--minViews`, `--minBookmarks`, `--anyOf`, `--q`, `--since`, `--until`, `--out` |
| `mentions:export-json` | Export mentions as JSON | `--keywordId`, `--platform`, `--status`, `--relevant`, `--sentiment`, `--intent`, `--automated`, `--personId`, `--includeMuted`, `--assigneeId`, `--snoozed`, `--excludeAuthors`, `--minRelevance`, `--minConfidence`, `--minFollowers`, `--maxFollowers`, `--isReply`, `--alertId`, `--viewId`, `--keywordKinds`, `--tags`, `--linkHosts`, `--platforms`, `--notPlatforms`, `--keywordIds`, `--groupIds`, `--notGroupIds`, `--notKeywordIds`, `--sentiments`, `--notSentiments`, `--intents`, `--notIntents`, `--notLinkHosts`, `--notTags`, `--languages`, `--notLanguages`, `--ratings`, `--notRatings`, `--minLikes`, `--minReposts`, `--minReplies`, `--minQuotes`, `--minViews`, `--minBookmarks`, `--anyOf`, `--q`, `--since`, `--until` |
| `mentions:get ` | Get a mention | |
| `mentions:search` | List mentions | `--keywordId`, `--platform`, `--status`, `--relevant`, `--sentiment`, `--intent`, `--automated`, `--personId`, `--includeMuted`, `--assigneeId`, `--snoozed`, `--excludeAuthors`, `--minRelevance`, `--minConfidence`, `--minFollowers`, `--maxFollowers`, `--isReply`, `--alertId`, `--viewId`, `--keywordKinds`, `--tags`, `--linkHosts`, `--platforms`, `--notPlatforms`, `--keywordIds`, `--groupIds`, `--notGroupIds`, `--notKeywordIds`, `--sentiments`, `--notSentiments`, `--intents`, `--notIntents`, `--notLinkHosts`, `--notTags`, `--languages`, `--notLanguages`, `--ratings`, `--notRatings`, `--minLikes`, `--minReposts`, `--minReplies`, `--minQuotes`, `--minViews`, `--minBookmarks`, `--anyOf`, `--q`, `--since`, `--until`, `--sort`, `--cursor`, `--limit` |
| `mentions:update ` | Update a mention | `--status`, `--assigneeId`, `--snoozedUntil`, `--note`, `--relevant`, `--sentiment`, `--json` |
| `mentions:watch` | Print each new mention as one JSON line, oldest first | the `mentions:search` filters except `--since`, `--until`, `--sort`, `--cursor`, `--limit`; `--interval`, `--from-start` |
| `views:create` | Create a view | `--name`, `--description`, `--filter`, `--json` |
| `views:delete ` | Delete a view | |
| `views:get ` | Get a view | |
| `views:list` | List views | |
| `views:update ` | Update a view | `--name`, `--description`, `--filter`, `--json` |
# People and segments
Work with the people who post about you, log outreach, and save segments.
Pass ids as arguments and everything else as flags with the API's field names. The [API reference](/api) explains each field, and `--help` on any command lists its flags. Every command also takes `--api-key`, `--api-url`, `--pretty` and `--table`.
```bash
rumoro people:list --platforms x --minFollowers 2000 --table
rumoro people:update aut_... --tags influencer --stage contacted
rumoro people:log-activity aut_... --channel email --note "Intro sent"
rumoro people:merge aut_... --into aut_...
rumoro segments:create --name "Large accounts" --filter '{"minFollowers":25000}'
```
| Command | What it does | Flags |
| --- | --- | --- |
| `people:activities ` | List outreach activities | |
| `people:delete-activity ` | Delete outreach | |
| `people:export` | Export people as CSV | `--platform`, `--q`, `--handle`, `--tag`, `--muted`, `--since`, `--segmentId`, `--platforms`, `--tags`, `--minFollowers`, `--maxFollowers`, `--minMentions`, `--minNegative`, `--intents`, `--notPlatforms`, `--notTags`, `--notIntents`, `--keywordKinds`, `--neverKeywordKinds`, `--newSinceDays`, `--linkHosts`, `--stages`, `--automated`, `--ownerIds`, `--sort`, `--out` |
| `people:get ` | Get a person | |
| `people:list` | List people | `--platform`, `--q`, `--handle`, `--tag`, `--muted`, `--since`, `--segmentId`, `--platforms`, `--tags`, `--minFollowers`, `--maxFollowers`, `--minMentions`, `--minNegative`, `--intents`, `--notPlatforms`, `--notTags`, `--notIntents`, `--keywordKinds`, `--neverKeywordKinds`, `--newSinceDays`, `--linkHosts`, `--stages`, `--automated`, `--ownerIds`, `--sort`, `--limit`, `--offset` |
| `people:log-activity ` | Log outreach | `--channel`, `--note`, `--occurredAt`, `--memberId`, `--json` |
| `people:merge ` | Merge people | `--into`, `--json` |
| `people:split ` | Split a person | |
| `people:update ` | Update a person | `--tags`, `--notes`, `--muted`, `--ownerId`, `--stage`, `--json` |
| `segments:create` | Create a segment | `--name`, `--description`, `--filter`, `--json` |
| `segments:delete ` | Delete a segment | |
| `segments:get ` | Get a segment | |
| `segments:list` | List segments | |
| `segments:update ` | Update a segment | `--name`, `--description`, `--filter`, `--json` |
# MCP server
Connect Claude Code and other MCP clients to your mentions. Sign in with OAuth or use an API key.
Rumoro's [Model Context Protocol](https://modelcontextprotocol.io) server is hosted, so there is nothing to install. Once your MCP client is connected, your agent can search and triage mentions, manage keywords and your company profile, read analytics, and set up alerts, people and segments. It follows the same rules as the dashboard.
```
https://mcp.rumoro.dev/mcp
```
- **Hosted, streamable HTTP, no session.** There is nothing to install. Clients must accept both `application/json` and `text/event-stream` answers.
- **OAuth or an API key.** Clients that support MCP authorization sign you in through the browser. Others send a key as a Bearer header. With `read` access the client sees only the read tools, with `write` it sees all.
- **55 tools** that match the [REST API](/conventions). They are listed under [Tools](/mcp/tools).
## Quick start
### Add the server
In Claude Code, run
```bash
claude mcp add --transport http rumoro https://mcp.rumoro.dev/mcp
```
Then type `/mcp`, choose `rumoro` and **Authenticate**. Sign in and approve the access the client asks for. If you belong to several workspaces, you also choose one. Other clients are under [Installation](#installation).
For CI, a server or a client without OAuth, create a key on the **API keys** page, `read` to look around or `write` to make changes. You only see it once.
### Ask
Try "Which mentions this week were negative, and what were people unhappy about?"
## Authentication
Each tool call carries a Bearer credential. The server handles both kinds the same way.
```
Authorization: Bearer
Authorization: Bearer ref_...
```
### OAuth 2.1
Sign-in follows the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization). A call without a credential gets `401` with `WWW-Authenticate: Bearer realm="rumoro-mcp", resource_metadata="https://mcp.rumoro.dev/.well-known/oauth-protected-resource"`. The client then registers itself (dynamic registration with PKCE) and opens the browser. You approve each sign-in on a consent page, and the token acts as you, within your role in that workspace. Access tokens last an hour. With `offline_access` the client also gets a refresh token that lasts 30 days and is replaced each time it is used. A sign-in without `write` works as `read`.
### API keys
Create keys on the **API keys** page or with [`POST /v1/api-keys`](/authentication). You see each key once, and it belongs to one workspace. When you revoke a key, the client stops working at its next call.
### Discovery
Connecting needs no credential. `initialize`, `tools/list` and the resources answer without one. A credential that is sent is always checked, and every `tools/call` needs one.
Add `https://mcp.rumoro.dev/mcp` as a custom connector and sign in when asked.
## Resources
| Resource | URI | What it contains |
| --- | --- | --- |
| `openapi` | `https://api.rumoro.dev/v1/openapi.json` | The REST API behind the tools, with all schemas |
| `agent-guide` | `https://mcp.rumoro.dev/mcp/guide` | How an agent connects, what each permission allows, and a review workflow |
Each tool has `readOnlyHint`, `destructiveHint` and `idempotentHint`, so a client knows which calls to confirm first. The seven destructive tools are marked under [Tools](/mcp/tools).
Registries can read the [server card](https://mcp.rumoro.dev/.well-known/mcp/server-card.json).
## How the tools behave
- Each tool runs the same operation as the REST API, with the same validation, errors and [rate limit](/rate-limits).
- `search_mentions` returns 10 mentions by default and `hasMore`, without a cursor. When there are more, narrow the filters or the time range.
- There are no export tools. Use the REST exports or the [CLI](/cli) for large lists.
- Write tools need a `write` key or sign-in. With `read` they don't appear at all.
- The server keeps no session between requests. Answers come as JSON or as a short event stream.
## Author data
`list_people`, `get_person` and the other people tools return the public facts Rumoro keeps about authors, such as name, handle, picture, follower count and profile details. They never return an email address, so `profile.email` is always `null` over MCP, even where the REST API shows one. [How it works](/how-it-works#author-data) explains what is stored and how people can ask to be removed.
## What you can ask
- "Summarise what people said about us on Hacker News and Reddit in the last 7 days."
- "Find questions about our pricing that nobody has answered, and draft a reply for each."
- "Which competitor got the most positive mentions this month, and why?"
- "Mark every mention from our own team as done."
- "Our keyword `driftwood` is noisy. Look at its health report and apply the best suggestion."
- "Create a daily digest of negative mentions for the support channel at 9:00 Berlin time."
- "Who are the people with more than 10,000 followers who mentioned us this month?"
## Documentation MCP server
The docs have their own read-only MCP server, and it needs no key.
```
https://docs.rumoro.dev/api/mcp
```
- **Tools.** `search_docs` searches the full text, `read_page` returns a page as Markdown by its path, such as `/quickstart`, and `list_pages` lists every page with its title and description.
- **Resources.** Every page at its `.mdx` address, plus `https://docs.rumoro.dev/llms.txt`.
- **Card.** [`/.well-known/mcp.json`](https://docs.rumoro.dev/.well-known/mcp.json).
## Security
- **Give only the access needed.** Use `read` to explore, and `write` only for an agent that should change things. No tool can change billing or API keys, though `get_usage` and `list_ledger` read the balance.
- **Same rules as the API.** Calls act as the key's workspace or the person who signed in, with the same checks and [rate limit](/rate-limits).
- **Keep keys out of repositories.** OAuth leaves no secret in client settings. With a key, use user-level settings or an environment variable, and revoke it if it leaks.
## Installation
With OAuth, a client only needs the URL. With a key, add the `Authorization: Bearer ref_...` header. In the [CLI](/cli), `rumoro mcp:config` prints these settings with your key filled in.
```bash
claude mcp add --transport http rumoro https://mcp.rumoro.dev/mcp
```
Then run `/mcp`, choose `rumoro` and **Authenticate**. Add `--scope project` to save the URL in the repository's `.mcp.json`, or `--scope user` to use it in every project. The [Claude Code page](https://rumoro.dev/claude) walks through it with example questions. With a key, run
```bash
claude mcp add --transport http rumoro https://mcp.rumoro.dev/mcp \
--header "Authorization: Bearer ref_..."
```
Add the server to `~/.cursor/mcp.json` for every project, or to `.cursor/mcp.json` for one. The [Cursor page](https://rumoro.dev/cursor) walks through it with example questions.
```json title=".cursor/mcp.json"
{
"mcpServers": {
"rumoro": {
"url": "https://mcp.rumoro.dev/mcp",
"headers": { "Authorization": "Bearer ref_..." }
}
}
}
```
Leave out `headers` to sign in with OAuth.
Run **MCP: Add Server**, choose **HTTP**, enter `https://mcp.rumoro.dev/mcp` and name it `rumoro`. With a key, use
```json title=".vscode/mcp.json"
{
"servers": {
"rumoro": {
"type": "http",
"url": "https://mcp.rumoro.dev/mcp",
"headers": { "Authorization": "Bearer ref_..." }
}
}
}
```
```bash
codex mcp add rumoro --url https://mcp.rumoro.dev/mcp
codex mcp login rumoro
```
The [Codex page](https://rumoro.dev/codex) walks through it with example questions. With a key, read it from an environment variable.
```bash
export RUMORO_API_KEY="ref_..."
codex mcp add rumoro --url https://mcp.rumoro.dev/mcp --bearer-token-env-var RUMORO_API_KEY
```
Open [grok.com/connectors](https://grok.com/connectors), choose **New Connector**, then **Custom**, and enter `https://mcp.rumoro.dev/mcp`. Grok runs the OAuth sign-in. On Business and Enterprise plans an admin provisions the connector first. The [Grok page](https://rumoro.dev/grok) walks through it with example questions.
Add the server under `mcp_servers`, then run `hermes mcp login rumoro`. Hermes registers itself with Rumoro, so there's no client ID to create.
```yaml title="~/.hermes/config.yaml"
mcp_servers:
rumoro:
url: "https://mcp.rumoro.dev/mcp"
auth: oauth
```
Check it with `hermes mcp test rumoro`, or run `/reload-mcp` in a session. The [Hermes page](https://rumoro.dev/hermes) walks through it with example questions. With a key, replace `auth: oauth` with `headers: { Authorization: "Bearer ${RUMORO_API_KEY}" }` and put `RUMORO_API_KEY` in `~/.hermes/.env`.
```bash
openclaw mcp add rumoro --url https://mcp.rumoro.dev/mcp --transport streamable-http --auth oauth
openclaw mcp login rumoro
openclaw mcp doctor rumoro --probe
```
The [OpenClaw page](https://rumoro.dev/openclaw) walks through it with example questions. To use an API key instead, install the [Rumoro skill](/integrations/openclaw).
Use the Cursor settings with the client's own file and URL key.
| Client | File | URL key |
| --- | --- | --- |
| Gemini CLI | `~/.gemini/settings.json` | `httpUrl` |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` | `serverUrl` |
| Any other | The client's MCP settings | `url`, plus `"type": "http"` |
## Help your agent find its way
Tell the agent what you track in `AGENTS.md`, `CLAUDE.md` or the client's rules file.
```markdown title="AGENTS.md"
## Rumoro
- Workspace: Driftwood (connected through the `rumoro` MCP server)
- Brand keyword `driftwood`, competitors `flagpole` and `switchboard`, topic `feature flags`
- "Relevant" means the classifier scored it 40 or more; matched counts include noise
- Start with `search_mentions` and a time range; use `get_analytics_*` for numbers over a period
- Only change keywords or alerts when asked; triage (status, assignee, notes) is fine
```
## Troubleshooting
| Problem | What to do |
| --- | --- |
| `401` "Missing or invalid credentials" | Sign in again, or check the header and that the key isn't revoked |
| The client asks to authenticate | Run its sign-in, for Claude Code `/mcp` and **Authenticate** |
| No tools are listed | Reload the client |
| "Unknown tool" for a write tool | The key or sign-in only has `read` |
| A list seems cut short | `search_mentions` returns 10 at a time. Narrow the filters, or use the REST API to page through everything. |
| `405` | Use streamable HTTP (`POST /mcp`), not the older SSE transport |
# Tools
All 55 MCP tools by area, with what each one does.
The server has 55 tools. Inputs, validation and responses match the REST API (see [Conventions](/conventions)), with four differences. `search_mentions` returns `{ "data", "hasMore" }` and no cursor, so an agent narrows its filters instead of paging. Delete tools return `{ "id", "deleted": true }`. `split_person` returns `{ "id", "unlinked" }`. `log_activity` returns the activity and the person.
A `read` key or OAuth grant lists only the read tools. Annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`) tell a client which calls to confirm with you. Wrong arguments return JSON-RPC error `-32602` naming the first problem.
## Mentions
| Tool | Access | What it does |
| --- | --- | --- |
| `search_mentions` | read | Find mentions by keyword, keyword kind or group, platform, status, relevance, confidence, sentiment, intent, language, text (`q`), time, person, assignee, followers, engagement, review stars, author tags, linked hosts, replies, bots (`automated`), an alert (`alertId`) or a saved view (`viewId`). `not` lists exclude and `anyOf` adds OR groups. Snoozed mentions stay hidden unless `snoozed` is true. `sort` is `newest` or `priority` (past 30 days). Returns 10 by default and up to 100 with `limit`. |
| `get_mention` | read | Fetches one mention, classification included (relevance, sentiment, intents, confidence, uncertain, note). |
| `update_mention` | write | Handle a mention with `status` (`open`, `ignored`, `done`), `assigneeId`, `snoozedUntil` and `note`, or correct the classifier with `relevant` and `sentiment`. `null` clears a field, and fields you leave out stay as they are. |
| `get_mention_stats` | read | Returns mention counts for the past N days (7 by default), by platform and by sentiment. |
## Keywords and filters
| Tool | Access | What it does |
| --- | --- | --- |
| `list_keywords` | read | Your keywords with stats, polling status, matching rules and context. Filter by text, kind, status, platform or group, in pages. |
| `get_keyword` | read | A keyword by id with its settings, monthly cap (`pausedForCap`), stats and polling status per platform. |
| `get_keyword_health` | read | Shows whether a keyword is worth its cost over `range` (`7d`, `30d`, `90d`). Returns a status with reasons, noise by platform and week, the cost, and suggestions with a `patch` you can pass to `update_keyword`. `ai: true` adds a rewritten context. |
| `add_keyword` | write | Start monitoring a keyword with `kind` (`brand`, `competitor`, `topic`), `platforms`, `matching` rules, a classifier `context`, a monthly `cap`, a `groupId` and `reviewSources` (App Store, Google Play, Trustpilot, Google Maps). |
| `update_keyword` | write | Change a keyword's platforms, mute, `kind`, `context`, `matching` rules, `cap`, group or review sources. Rules only affect new mentions. |
| `delete_keyword` | write, destructive | Delete a keyword and its mentions. This cannot be undone, and muting keeps the mentions. |
| `get_filters` | read | The noise filters every keyword goes through before a mention is stored. They cover excluded terms, authors and GitHub repositories, and allowed or blocked subreddits. |
| `update_filters` | write | Change the workspace filters. A list you leave out stays as it is, and an empty list clears it. |
## Groups
| Tool | Access | What it does |
| --- | --- | --- |
| `list_groups` | read | Lists the workspace's keyword groups, the default first. |
| `get_group` | read | Returns a keyword group by id, with its number of keywords. |
| `create_group` | write | Create a keyword group with a `name`, an optional `externalId` (your own id, such as a client's) and an optional `context`, a company description the classifier reads instead of the workspace profile for this group's keywords. |
| `update_group` | write | Rename a group or change its `externalId` or `context`. `null` clears a field. The default group takes no context. |
| `delete_group` | write, destructive | Delete a keyword group with all its keywords and their mentions. The default group cannot be deleted. |
## Company
| Tool | Access | What it does |
| --- | --- | --- |
| `get_company` | read | What the classifier knows about the company, with the profile, own accounts and the `context` text it reads to score relevance. |
| `update_company` | write | Change the company profile (`name`, `description`, `useCases`, `accounts`) or set the classifier `context` yourself. |
## Workspace and usage
| Tool | Access | What it does |
| --- | --- | --- |
| `whoami` | read | Returns the workspace this credential works in, the kind of credential (an API key or an OAuth sign-in), whether it can write, and the signed-in user if there is one. |
| `list_members` | read | Lists workspace members with their role (owner, admin, member), email and user id. |
| `get_usage` | read | The prepaid balance, daily spend and days left, running and paused keywords, matches today and over 30 days, and whether tracking is stopped or the balance is low. |
| `get_usage_breakdown` | read | Usage and charges in US cents over a `range` (`7d`, `30d`, `90d`) or a calendar `month`, grouped `by` keyword, group, platform or day, with totals. In pages. |
| `list_ledger` | read | All changes to the prepaid balance, latest first. Welcome credit, top-ups, refunds, daily debits and adjustments, in pages. |
## Analytics
Each of the five reads one period, set with `range` (`7d`, `30d`, `90d`, `365d`) or with `from` and `to`. They also take `keywordIds`, `platforms`, `timezone` (IANA, UTC by default) and `compare`, which adds the period just before.
| Tool | Access | What it does |
| --- | --- | --- |
| `get_analytics_summary` | read | The main counts. Matched and relevant mentions, posts and people, sentiment, buying intent and questions, estimated reach, and triage. |
| `get_analytics_series` | read | Counts per hour, day, week or month (`bucket`), as one series or split `by` platform, keyword or sentiment. |
| `get_analytics_breakdown` | read | A table grouped `by` platform, keyword, sentiment, intent, status, hour, person or language, up to 50 rows. |
| `get_share_of_voice` | read | Your brand compared with competitors. Each keyword's counts and its part of all brand and competitor matches. |
| `get_reviews_report` | read | App Store, Google Play, Trustpilot and Google reviews. Totals, star distribution, tags of unhappy reviews, and average stars per review page over time. |
## Alerts, channels and attention
[Alerts](/alerts) explains alerts and channels, and [Webhooks](/webhooks) the payloads.
| Tool | Access | What it does |
| --- | --- | --- |
| `list_alerts` | read | All alerts with their filter, mode, schedule and channels. |
| `get_alert` | read | Fetches an alert by id. |
| `create_alert` | write | Create an alert. `mode` is `instant`, `hourly` (no email channel), `daily` at `schedule.hour` in `schedule.timezone`, or `weekly` on `schedule.weekday`. The `filter` can use keywords, platforms, relevance, sentiment, intent, authors, followers, tags or linked hosts. `channelIds` come from `list_channels`. |
| `update_alert` | write | Changes an alert's name, enabled, mode, schedule, filter or channelIds. filter and channelIds are replaced in full. |
| `delete_alert` | write, destructive | Delete an alert. Its channels remain. |
| `mute_authors` | write | Adds authors to an alert's muted list and keeps the rest of the filter. |
| `unmute_authors` | write | Takes authors off an alert's muted list and keeps the rest of the filter. |
| `list_channels` | read | Lists alert destinations (Slack channels, Telegram chats, email lists, webhooks). |
| `list_attention` | read | What someone should look at now. A mention spike, a jump in negative sentiment, a keyword that became noisy, or a failing channel. Open items by default. |
| `dismiss_attention` | write | Moves an attention item (att_... from list_attention) out of the open list for as long as its condition lasts. |
## People
| Tool | Access | What it does |
| --- | --- | --- |
| `list_people` | read | The authors of your mentions, with counts, sentiment and outreach. Filter by platform, name, handle, segment, stage, owner or bots, and sort by mentions or recency. |
| `get_person` | read | A person with counts, tags, notes, outreach and public profile. `profile.email` is always null. |
| `update_person` | write | Changes your workspace's details on a person. |
| `merge_people` | write, destructive | Tells your workspace that two accounts are the same individual by merging account `id` into person `into`. |
| `split_person` | write | Reverses a merge, so the account is a separate person again in your workspace. |
| `list_activities` | read | The outreach log for one person, latest first, with who reached out, the channel, the time and a note. |
| `log_activity` | write | Record that someone contacted a person. If the person has no owner, the first contact sets one and moves them to `contacted`. |
| `delete_activity` | write, destructive | Deletes an outreach activity that was logged by mistake. |
## Segments and views
| Tool | Access | What it does |
| --- | --- | --- |
| `list_segments` | read | Saved audience segments with their current size, plus presets for `create_segment`. |
| `create_segment` | write | Save an audience segment with a name and a filter on platforms, tags, followers, mention counts, intents, keyword kinds, first seen or linked hosts. Members are worked out on each read. |
| `update_segment` | write | Changes a saved segment's name, description or filter. |
| `delete_segment` | write, destructive | Deletes a saved segment. |
| `list_views` | read | Saved views, which are named mention filters. Pass an id as `viewId` to `search_mentions`. |
| `create_view` | write | Save a view with a name and a `search_mentions` filter, including `not` lists and `anyOf`. It shows whatever matches when read. |
| `update_view` | write | Changes a saved view's name, description or filter. |
| `delete_view` | write, destructive | Deletes a saved view and leaves its mentions untouched. |
# OpenClaw
Add the Rumoro skill to OpenClaw and ask it in plain words for your mentions, keywords, alerts and numbers.
[OpenClaw](https://openclaw.ai) is a personal agent that runs on your own machine. With the `rumoro` skill it can answer "what did people say about us this week?" from your real mentions. It can also add keywords, close out mentions, set up alerts and read your analytics. You need a Rumoro API key and a running OpenClaw gateway. To connect without a key, add Rumoro's [MCP server](/mcp#openclaw) instead; the [OpenClaw page](https://rumoro.dev/openclaw) shows both.
| | |
| --- | --- |
| Skill name | `rumoro` |
| Install | `openclaw skills install @markolo10/rumoro --global` |
| Key | `RUMORO_API_KEY` |
| API | `https://api.rumoro.dev/v1` |
What the skill lets OpenClaw do.
| Area | For example |
| --- | --- |
| Keywords | Add brand, competitor and topic keywords, narrow noisy ones, read their health |
| Mentions | Search with any filter, summarize, mark done or ignored, assign, snooze, correct a score |
| People | Find the people with the most reach, tag them, log outreach |
| Alerts | Instant alerts and hourly, daily or weekly digests to Slack, Telegram, email or a webhook |
| Analytics | Summary, trends, breakdowns and share of voice |
| Account | Balance, costs per keyword, the workspace the key belongs to |
The skill is short. For anything it doesn't cover, it reads the [agent guide](https://mcp.rumoro.dev/mcp/guide) and the [OpenAPI document](https://api.rumoro.dev/v1/openapi.json), so it stays in step with the API.
## Step 1. Install the skill
```bash
openclaw skills install @markolo10/rumoro --global
```
`--global` puts it in the shared skills folder that every agent on the machine reads. Leave it out to install into the current workspace only.
Create a key on the [API keys](https://app.rumoro.dev) page. A `write` key covers the whole skill. A `read` key can search and read but change nothing. Then give it to OpenClaw, either in its environment file
```bash
echo 'RUMORO_API_KEY=ref_...' >> ~/.openclaw/.env
```
or in `~/.openclaw/openclaw.json`, since the skill names `RUMORO_API_KEY` as its main variable.
```json5
{
skills: {
entries: {
rumoro: { apiKey: "ref_..." },
},
},
}
```
If the gateway was already running, restart it so it reads the key.
```bash
openclaw gateway restart
openclaw skills check
```
Then ask OpenClaw something small.
> Which Rumoro keywords do I have?
It calls `GET /v1/keywords`. An empty list means the key works and nothing is tracked yet.
## Step 2. Ask
Say what you want. The skill knows which calls to make.
> Sum up this week's Hacker News talk about us. Which threads need a reply?
OpenClaw asks for relevant Hacker News mentions from the past seven days, sorted by priority, and summarizes each one with the author, sentiment, intent and link. Other requests it handles.
- "Add `driftwood` as a brand keyword and `netlify` as a competitor, limited to GitHub and Hacker News."
- "Show bug reports from the last day, and mark the fixed ones done."
- "Send negative mentions to the #support Slack channel as they come in."
- "Email me a digest every weekday at 8:30 New York time."
- "Which platforms brought the most mentions this month, and how do we compare with Netlify?"
- "Find the ten most-followed people who mentioned us and tag the customers."
- "This keyword is noisy. Check its health and fix it."
Slack and Telegram channels are connected in the dashboard, so OpenClaw picks one you already have. It can create email and webhook channels itself. It asks you first before deleting anything, rotating a webhook secret or merging people.
## Step 3. Automate
### A morning summary
An OpenClaw automation runs a prompt on a schedule and posts the answer to a channel.
```bash
openclaw automations create "30 8 * * 1-5" \
"Use the rumoro skill. List the relevant mentions from the last 24 hours by sentiment, and point out questions and buying signals I should answer, with links." \
--name "Rumoro morning summary" \
--tz "America/New_York" \
--session isolated \
--announce \
--channel slack \
--to "channel:C0123456789"
```
Rumoro's own [daily digest](/alerts) sends the top mentions without an agent. Use the automation when you want OpenClaw's take, or a different question each morning.
### Answer a mention as it arrives
A Rumoro [webhook channel](/webhooks) can wake OpenClaw for each mention. Turn on hooks in `~/.openclaw/openclaw.json` and add a mapping for Rumoro's payload.
```json5
{
hooks: {
enabled: true,
token: "",
path: "/hooks",
mappings: [
{
id: "rumoro",
match: { path: "rumoro" },
action: "agent",
name: "Rumoro {{event}}",
messageTemplate: "New Rumoro mention on {{data.post.platform}} by {{data.author.name}}, sentiment {{data.classification.sentiment}}. {{data.post.text}} {{data.post.url}} Should we answer? Draft a reply.",
deliver: true,
channel: "slack",
to: "channel:C0123456789",
},
],
},
}
```
Run `openclaw config validate` and `openclaw gateway restart`. The hook URL has to be reachable over https from the internet, for example through a tunnel, because Rumoro can't reach `localhost`. Then ask OpenClaw to connect it.
> Add a Rumoro webhook for https://gateway.example.com/hooks/rumoro that sends `Authorization: Bearer `, and alert it right away about every negative mention.
Each negative mention now starts an OpenClaw run, and the draft appears in Slack. Rumoro counts any `2xx` answer within 10 seconds as delivered. OpenClaw wraps the post text as outside content before the model reads it, so leave `allowUnsafeExternalContent` off.
## If it fails
**`401` from the API.** Check the key on the API keys page. If it is mistyped or revoked, fix it and restart the gateway.
**The skill is missing.** `openclaw skills list` should show `rumoro`. If not, install it again. `openclaw skills check` names any missing requirement.
**`403 read_only_key`.** Changes need a `write` key. Create one.
**`402`.** The balance can't cover the change, such as one more keyword-day. Top up on the Billing page. Nothing is retried.
**Sandboxed agents.** The key set in `skills.entries` only reaches agents that run on the host. Pass `RUMORO_API_KEY` to the sandbox separately.
## Related
# Migrate from Octolens
Switch from Octolens by changing two settings, copy your keywords, feeds and filters with one script, and move your alerts.
Rumoro speaks Octolens' `/api/v2` API at `https://api.rumoro.dev/compat/octolens`, with the same paths and JSON. Code you wrote for Octolens, the `octolens` CLI and integrations built on its API keep working after you change the base URL and the API key. For prices and features side by side, see [Octolens and Rumoro compared](https://rumoro.dev/alternatives/octolens).
| | Octolens | Rumoro |
| --- | --- | --- |
| Base URL | `https://app.octolens.com` | `https://api.rumoro.dev/compat/octolens` |
| Key | `Bearer ` | `Bearer ref_...` |
| Paths, JSON, errors | `/api/v2/...` | The same |
| Rate limit | 500 requests an hour | 600 a minute per workspace |
| Price | Plans with a monthly mention quota | $5 per keyword a month and $0.008 per matched mention, prepaid, no plan |
Keywords you create through this API are normal Rumoro keywords, with the same balance checks and alerts. Switch to the [`/v1` API](/api) when you want keyword groups, people, share of voice, Telegram alerts or the MCP server.
## Step 1. Change the base URL
Create a key on the **API keys** page, then point your tools at Rumoro.
### The octolens CLI
```bash
export OCTOLENS_BASE_URL=https://api.rumoro.dev/compat/octolens
export OCTOLENS_API_KEY=ref_...
octolens whoami --json
octolens keywords list
octolens mentions list --source twitter
```
`whoami` shows your Rumoro workspace, and you don't need `octolens login`. Tested with `octolens` 0.1.8.
### Your own code
```ts
const BASE = 'https://api.rumoro.dev/compat/octolens'; // was https://app.octolens.com
const headers = { Authorization: `Bearer ${process.env.RUMORO_API_KEY}`, 'Content-Type': 'application/json' };
const res = await fetch(`${BASE}/api/v2/mentions`, { method: 'POST', headers, body: JSON.stringify({ limit: 20 }) });
const { data, pagination } = await res.json();
```
HTTP steps in Zapier or n8n change the same way.
## Step 2. Copy your workspace
Set both keys and run the script once. It copies your company profile, keywords, feeds and global filters, and tells you about anything it can't copy.
```js title="copy-from-octolens.mjs"
// node copy-from-octolens.mjs (Node 18 or newer, nothing to install)
const FROM = { base: process.env.OCTOLENS_BASE_URL ?? 'https://app.octolens.com', key: process.env.OCTOLENS_API_KEY };
const TO = { base: process.env.RUMORO_COMPAT_URL ?? 'https://api.rumoro.dev/compat/octolens', key: process.env.RUMORO_API_KEY };
async function api(side, method, path, body) {
const response = await fetch(`${side.base}/api/v2${path}`, {
method,
headers: { Authorization: `Bearer ${side.key}`, 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(`${method} ${path}: ${json.error?.code ?? response.status} ${json.error?.message ?? ''}`.trim());
return json;
}
async function attempt(label, work) {
try { return await work(); } catch (error) { console.warn(`${label}: not copied (${error.message})`); return undefined; }
}
// Company profile. Rumoro scores relevance against it.
const company = await api(FROM, 'GET', '/org/company');
const profileFields = ['name', 'description', 'productUseCases', 'competitors', 'relevanceContext', 'relevanceGuidelines', 'twitter', 'linkedin'];
const profile = Object.fromEntries(profileFields.filter((field) => company[field]).map((field) => [field, company[field]]));
await attempt('company profile', () => api(TO, 'PATCH', '/org/company', profile));
// Keywords. Remember each new id, because the feeds refer to keywords by id.
const keywordIds = new Map();
for (const keyword of (await api(FROM, 'GET', '/keywords')).data) {
const { id, isSubReddit, symbolSensitive, ...rest } = keyword;
if (isSubReddit) { console.warn(`keyword ${keyword.keyword}: skipped (subreddit keywords are a global filter in Rumoro)`); continue; }
const fields = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== null)); // drop null fields, which a create doesn't accept
const created = await attempt(`keyword ${keyword.keyword}`, () => api(TO, 'POST', '/keywords', fields));
if (created) keywordIds.set(String(id), String(created.id));
}
// Feeds. If none of a feed's keywords made it across, skip the feed so it doesn't suddenly match everything.
const translate = (condition) => {
if (condition.field !== 'Keywords') return { condition, lost: false };
const values = String(condition.values).split(',').map((value) => keywordIds.get(value.trim())).filter(Boolean);
const excludes = String(condition.operator ?? 'in').startsWith('not');
return { condition: { ...condition, values: values.join(',') }, lost: values.length === 0 && !excludes };
};
for (const feed of (await api(FROM, 'GET', '/feeds')).data) {
if (feed.isDefault) continue;
let lost = false;
const conditions = (list) => list.map(translate).filter((item) => { lost ||= item.lost; return item.condition.field !== 'Keywords' || item.condition.values; }).map((item) => item.condition);
const body = { name: feed.name };
if (feed.simpleFilters) body.simpleFilters = { conditions: conditions(feed.simpleFilters.conditions) };
if (feed.advancedFilters) body.advancedFilters = { ...feed.advancedFilters,
groups: feed.advancedFilters.groups.map((group) => ({ ...group, conditions: conditions(group.conditions) })).filter((group) => group.conditions.length) };
if (lost) { console.warn(`feed ${feed.name}: skipped (none of its keywords was copied)`); continue; }
await attempt(`feed ${feed.name}`, () => api(TO, 'POST', '/feeds', body));
}
// Workspace-wide filters.
await attempt('global filters', async () => api(TO, 'PATCH', '/filters/global', await api(FROM, 'GET', '/filters/global')));
console.log('done');
```
```bash
OCTOLENS_API_KEY=... RUMORO_API_KEY=ref_... node copy-from-octolens.mjs
```
Copied keywords start collecting right away. On X, Bluesky, GitHub, Stack Overflow, TikTok and Hacker News they also fetch up to 10 matches from the last 30 days, and on Instagram from the last 10 days.
Rumoro only creates a keyword while your balance covers one more day of all running keywords. The welcome credit of $5.80 counts toward that. Keywords that don't fit are reported as `QUOTA_EXCEEDED`. Add funds and run the script again, and keywords already copied won't be duplicated. The script also reports keywords that would only track Product Hunt, Medium, newsletters, podcasts, Reddit comments or review pages.
## Step 3. Move your notifications
Octolens notifications become Rumoro [alerts](/alerts). Create them under **Alerts** or with [`POST /v1/alerts`](/api/alerts/create-alert). Alerts go to Slack, Telegram, email or a webhook.
| Octolens | Rumoro alert |
| --- | --- |
| `frequency: hourly` or `hourlyAtTopOfHour`, `deliveryMode: batch` | `mode: hourly`, sent five minutes past each UTC hour and skipped when empty. Not available by email. |
| `frequency: daily` or `weekly`, with `time`, `timezone`, `dayOfWeek` | `mode: daily` or `weekly`, with `schedule.hour`, `minute`, `timezone`, `weekday` |
| `deliveryMode: individual`, and every webhook | `mode: instant`, one message per mention |
Rumoro webhooks are signed with `X-Mentions-Signature-V2` (see [Webhooks](/webhooks)), so add the check to your handler when you move it.
## Step 4. Switch over
1. Run step 2, keep both tools running for a day, and compare the results of `POST /api/v2/mentions`.
2. Point your code and the CLI at Rumoro.
3. Set up alerts to replace your notifications, and test each one.
4. Pause your Octolens keywords, then cancel your Octolens plan.
## Differences in the compatible API
| Topic | How it works in Rumoro |
| --- | --- |
| Ids | Numbers, given the first time an item is seen and kept after that. `sourceId` is the Rumoro id (`mm_...`), and `GET /api/v2/mentions/{sourceId}` accepts the number too. |
| Rows | One row per matched keyword, where Octolens combines them into one. |
| Titles and images | `title` and `imageUrl` when the platform has them |
| Lists | Only relevant mentions unless `includeAll: true`. Muted people and snoozed mentions are left out. At most 50 keyword ids. Pages of 20, up to 100. |
| Relevance | `relevanceScore` 0 means a score of 70 or more, 1 means 40 to 69, 2 means lower. `relevant` means 40 or more. Writing 0 or 1 sets the score to 100, 2 marks a mention not relevant, and 3 restores the original. Filters accept high, high and medium, low, or all. Other combinations return `501`. |
| Tags | Rumoro's intents under Octolens' names (`product_question`, `user_feedback` and so on), plus `own_brand_mention`, `competitor_mention` and `ai_generated` |
| Platforms | `twitter` is X, `dev` is DEV, and `reddit_comment` is Reddit replies. Keywords created or updated here always include Instagram. Product Hunt, Medium, newsletters and podcasts aren't searched. Review pages are added per keyword in the dashboard or through `/v1`. |
| Keyword matching | Whole phrases only (`symbolSensitive` has no effect). `*` wildcards work only at the start or end. `isSubReddit` returns `501`, so use `positiveSubreddits`. |
| Filters | AND and OR groups become [`anyOf`](/conventions#or-across-groups-anyof), up to 10 alternatives. `Content` returns `501`, so use `search`. `TwitterFollowerCount` accepts `>=`, `<=` and `=`. |
| Global filters | `add` skips duplicates and lists what it refused under `dropped`. Emptying a list needs `allowEmpty: true`. |
| Feeds | Saved as views, without the icon. Destinations return `501`. A view this API can't represent is read-only here (`409 FEED_FILTER_WRITE_CONFLICT`). |
| Exports | Up to 5,000 mentions. `author` exports one person's mentions. Read-only keys can export. |
| Keywords | 2 to 80 characters. Sending an existing term returns that keyword, and with `allowDuplicate: true` it returns `409 ITEM_EXISTS`. |
| Company | `classificationGuidelines` returns `501`, so use `relevanceGuidelines`. |
| Analytics | UTC days, 30 by default, high and medium relevance unless you set `relevance=0,1,2`. Tag or sentiment filters return `501`. |
| Paths | Paths outside `/api/v2`, or with a trailing slash, return `404`. Unknown `/api/v2` paths return `501`. |
| Usage | `mentions.limit` is how many mentions your balance still covers this month. |
| Rate limit | Shared with `/v1` and MCP, with `X-RateLimit-*` headers |
## Not available
These return `501 FEATURE_DISABLED`. Agency workspaces, setup scans, the AI filter wizard and recommendations, on-demand search, keyword suggestions and estimates, notifications and export download links. Several of them have an equivalent in `/v1`, such as [keyword health](/guides/keyword-health) and [alerts](/alerts).
# Webhooks
Get mentions and digests as signed requests to your own URL. Setup, retries, signature checks and every event.
A webhook is a [channel](/alerts) that sends to a URL you control. Every alert rule attached to it sends one signed JSON `POST` for each matching mention (instant rules) or for each digest (hourly, daily and weekly rules).
- One URL can serve many rules. Use the payload's `event` to tell them apart.
- A delivery tells you something happened. The mention itself is always at `GET /v1/mentions/{id}`.
- Reply with a 2xx right away and do the work afterwards. Rumoro ignores the response body.
## Setup
1. Create a webhook channel. The response contains the signing secret, which you only see once.
2. Add alert rules that send to it, each with its own filter and `event` name.
3. Rumoro sends a `POST` for every matching mention or digest. Reply with a 2xx within 10 seconds.
### Create the channel
```ts tab="TypeScript"
const { data: channel } = await rumoro.createChannel({
body: { kind: 'webhook', url: 'https://hooks.example.dev/rumoro', label: 'Support bot', headers: { 'X-Team': 'support' } },
});
```
```python tab="Python"
channel = rumoro.channels.create(
kind="webhook", url="https://hooks.example.dev/rumoro", label="Support bot", headers={"X-Team": "support"}
)
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/channels \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "kind": "webhook", "url": "https://hooks.example.dev/rumoro", "label": "Support bot",
"headers": { "X-Team": "support" } }'
```
```json
{
"id": "dest_...",
"label": "Support bot",
"stats": { "alerts": 0, "activeAlerts": 0, "lastDeliveryAt": null, "last7d": { "total": 0, "failed": 0 } },
"createdAt": "2026-10-04T09:12:44.208Z",
"kind": "webhook",
"config": {
"url": "https://hooks.example.dev/rumoro",
"headers": { "x-team": "support" },
"events": [],
"secret": "whsec_..."
}
}
```
You get `config.secret` only in this response and after a rotation. Without a `label`, the URL's host is used. The URL must be public and use `https`.
Rumoro sends your `headers` with every request. You can set up to 20. Names are letters, digits and hyphens (stored lowercase), and values are up to 1,024 printable ASCII characters. Names that would clash with Rumoro's own signing or transport headers are refused with `400`.
`PATCH /v1/channels/{id}` changes the URL, label or headers. Changing the URL or headers, or turning the channel off, cancels deliveries that haven't gone out yet.
### Attach a rule
```ts tab="TypeScript"
await rumoro.createAlert({
body: { name: 'Bug reports', mode: 'instant', event: 'mention.bug_report', filter: { intents: ['bug_report'] }, channelIds: ['dest_...'] },
});
```
```python tab="Python"
rumoro.alerts.create(
name="Bug reports", mode="instant", event="mention.bug_report", filter={"intents": ["bug_report"]}, channelIds=["dest_..."]
)
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/alerts \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Bug reports", "mode": "instant", "event": "mention.bug_report",
"filter": { "intents": ["bug_report"] }, "channelIds": ["dest_..."] }'
```
`event` can be any lowercase name with dots, up to 60 characters. Instant rules default to `mention.matched` and digest rules to `digest`.
## Retries
When your URL doesn't reply with a 2xx within 10 seconds, Rumoro decides by the reason.
- **Temporary problems** are a timeout, a network error, `408`, `429` or a `5xx`. Instant deliveries are tried up to 5 times, about 30 seconds apart, or later if your `Retry-After` asks for it. Every attempt has the same `id`.
- **Permanent problems** are a redirect or any other `4xx`. Rumoro doesn't retry and records the status in the delivery log.
- **Digests** are retried per channel every 5 minutes for up to 6 hours. Hourly digests are retried until 45 minutes past the hour after they were due.
- A payload larger than 1 MiB is never sent.
## Duplicates
A retry has the same `id` as the first attempt, so keep the ids you have processed and skip repeats. A replay (`POST /v1/deliveries/{id}/replay`) comes with a new `id`.
Each delivery is one keyword match. A post that matches two of your keywords arrives twice, with two mention ids and two `keyword` objects. Mentions marked ignored or done, and posts by muted authors, are not sent. Matches scored as noise only reach rules whose `minRelevance` is below 40.
## Signature verification
Every request carries these headers.
| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |
| `X-Mentions-Timestamp` | When Rumoro built the request, in Unix seconds |
| `X-Mentions-Signature-V2` | `v2=` and the hex HMAC-SHA256 of the timestamp, a dot and the raw body |
| `X-Mentions-Signature` | The hex HMAC-SHA256 of the raw body only. Kept for older code, without replay protection. |
### Verify a request
Use the whole secret as the HMAC key, including `whsec_`. Check the V2 signature against the raw body before you parse it, compare in constant time, and reject timestamps more than five minutes from now. Because the timestamp is part of the signature, a captured request can't be reused later. Every retry gets a new timestamp and signature.
```ts
import { createHmac, timingSafeEqual } from 'node:crypto';
// body: the raw request body as received. secret: your channel secret, including "whsec_".
export function isFromRumoro(body: Buffer, timestamp: string | undefined, signature: string | undefined, secret: string) {
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || !signature || !(age <= 300)) return false;
const expected = Buffer.from('v2=' + createHmac('sha256', secret).update(timestamp + '.').update(body).digest('hex'));
const given = Buffer.from(signature);
return expected.length === given.length && timingSafeEqual(expected, given);
}
// isFromRumoro(rawBody, req.headers['x-mentions-timestamp'], req.headers['x-mentions-signature-v2'], secret)
```
```python
import hashlib
import hmac
import time
# body: the raw request body as bytes. secret: your channel secret, including "whsec_".
def is_from_rumoro(body: bytes, timestamp: str | None, signature: str | None, secret: str) -> bool:
if not timestamp or not signature or not timestamp.isdigit():
return False
if abs(time.time() - int(timestamp)) > 300:
return False
digest = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest("v2=" + digest, signature)
```
### Rotate the secret
`POST /v1/channels/{id}/rotate-secret` creates a new secret and shows it once. The old secret stops working at the same moment, so update your code right away.
## The payload
All events use the same envelope.
```json
{
"id": "dlv_...",
"event": "mention.bug_report",
"createdAt": "2026-10-04T09:12:44.208Z",
"alert": { "id": "feed_...", "name": "Bug reports" },
"data": { }
}
```
| Field | Meaning |
| --- | --- |
| `id` | The delivery id. In the delivery log, scheduled daily and hourly digests are listed under the rule and period instead (`feed_…:2026-10-04`, `feed_…:h:2026-10-04T09`). |
| `event` | The rule's event name, an [account event](/webhooks/account-events), or `test` |
| `createdAt` | When Rumoro built the request |
| `alert` | The rule that sent it. `alert.id` is `null` for a channel test, and `alert` is `null` for account events. |
| `data` | A [mention](/webhooks/mention-events), a [digest](/webhooks/digest-events) or an [account event](/webhooks/account-events). For a test it is `{ "message" }`. |
## Events
### Rule events
The dashboard's **Webhooks** page offers these presets. Each one is a normal alert rule.
| Event | Mode | Filter |
| --- | --- | --- |
| `mention.matched` | instant | No filter, every relevant mention |
| `mention.high_relevance` | instant | `minRelevance: 80` |
| `mention.negative`, `mention.positive` | instant | `sentiments: ["negative"]` or `["positive"]` |
| `mention.buy_intent`, `mention.question`, `mention.complaint`, `mention.comparison`, `mention.churn_intent`, `mention.bug_report`, `mention.pricing` | instant | `intents` with that intent |
| `review.negative` | instant | `ratings: [1, 2]` |
| `digest` | daily | No filter, one summary a day |
| `test` | on request | Sent by the test calls below |
### Account events
[Account events](/webhooks/account-events) don't use rules. A channel subscribes to them by name, with `events` on `POST` or `PATCH /v1/channels`.
| Event | Sent when |
| --- | --- |
| `keyword.capped` | A keyword hit its monthly mention cap |
| `keyword.paused_for_balance` | A keyword stopped because the balance ran out |
| `keyword.resumed` | A keyword is collecting again |
| `wallet.low` | The balance dropped to 20% of the last credit |
| `wallet.paused` | All keywords stopped for lack of balance |
| `wallet.resumed` | A credit restarted them |
| `mention.spike` | A keyword got far more mentions than usual in the last hour |
| `sentiment.negative_spike` | A keyword's mentions turned negative over the last day |
| `keyword.noisy` | Most of a keyword's matches are noise |
| `channel.failing` | A channel's recent deliveries all failed |
The last four come from [Needs attention](/guides/attention).
## Testing and the delivery log
| Call | What it does |
| --- | --- |
| `POST /v1/channels/{id}/test` | Sends an `event: "test"` request to this channel and returns `{ "outcomes": [{ "channelId", "ok", "error" }] }` |
| `POST /v1/alerts/{id}/test` | Does the same for every channel of the rule, with `alert` set |
| `GET /v1/channels/{id}/deliveries` | Recent deliveries. Each has `kind` (`mention`, `digest` or `event`), `event`, `status` (`pending`, `delivered` or `failed`), `attempts`, the last `error`, `sentAt`, the rule's name and a short mention summary. Tests are not listed. |
| `POST /v1/deliveries/{id}/retry` | Sends a failed or held delivery again with the same `id` |
| `POST /v1/deliveries/{id}/replay` | Sends a finished delivery again with a new `id` |
| `DELETE /v1/channels/{id}` | Removes the channel from all rules and returns `204` |
Retry and replay need a body with a `requestId` (a UUID), such as `{ "requestId": "2f0b7c1e-4d93-4a8e-9b61-5c3e7a0d1f24" }`, and return `202`. They are not in the API reference. [Conventions](/conventions#outside-the-reference) explains how these endpoints answer.
With the CLI, use `rumoro channels:test dest_...` and `rumoro channels:deliveries dest_...`. The other calls are in the [API reference](/api/alerts/create-channel).
# Mention events
The request an instant rule sends for each new mention, field by field.
An `instant` rule sends one request per mention that passes its filter, once it is scored. `data` is the mention as `GET /v1/mentions/{id}` returns it ([reference](/api/mentions/get-mention)).
```json
{
"id": "dlv_0cf858bebb77495dbf6eec7e063517cc",
"event": "mention.matched",
"createdAt": "2026-10-02T11:01:48.751Z",
"alert": { "id": "feed_68f8c27f90a04454af662c2866f6f49d", "name": "compare instant" },
"data": {
"id": "mm_a1951e9f623c410599a710f5cd55f28b",
"status": "open",
"relevant": true,
"delivered": false,
"priority": 60,
"keyword": {
"id": "kw_8de5ae0ca00a4ab1a5ed297db85275fa",
"term": "n8n",
"group": { "id": "grp_b4a976f98c3946d7bb83554caec56bc2", "name": "Default", "externalId": null, "isDefault": true }
},
"post": {
"platform": "bluesky",
"url": "https://bsky.app/profile/flowify.bsky.social/post/3mwv7rsiw6g2g",
"text": "OpenAI vient de lancer Dots, à 200 $ par mois, soit trois fois le prix du plan standard. …",
"title": null,
"imageUrl": null,
"links": [],
"engagement": null,
"publishedAt": "2026-10-02T11:00:49.000Z",
"replyTo": null
},
"review": null,
"author": {
"id": "aut_dee30396a6ca409fb0fec8bb633ecb12",
"name": "flowify.bsky.social",
"handle": "@flowify.bsky.social",
"url": "https://bsky.app/profile/flowify.bsky.social",
"avatarUrl": "https://cdn.bsky.app/img/avatar/plain/did:plc:l4evdzsnuhihppfkm4wzcjl6/…",
"followers": 5,
"tags": []
},
"classification": {
"relevance": 72,
"sentiment": "neutral",
"intents": ["buy_intent"],
"automated": false,
"language": "fr",
"confidence": 0.809,
"uncertain": false,
"note": "The post mentions n8n as a solution to automate workflows and avoid manual copy-pasting, indicating its use in a business context.",
"failed": false,
"feedback": null
},
"triage": { "assignee": null, "snoozedUntil": null, "note": null },
"createdAt": "2026-10-02T11:01:45.772Z"
}
}
```
`post.engagement` holds the counts at the time Rumoro found the post. `review` is set for reviews only.
## What to expect
- **Relevant by default.** Without `minRelevance`, a rule sends mentions scored 40 or more. Lower it (`0` sends all) to get noise too, with `data.relevant: false`. Email keeps the 40 minimum.
- **Always scored.** `data.classification` is never `null` here.
- **No look-back.** A new keyword's older posts go to the feed and digests only.
- **Your triage counts.** A mention you mark done, ignored or not relevant first isn't sent.
- **`data.delivered`** reflects when the request was built, so it's usually `false`.
- **`data.id`** is the mention id, for `PATCH /v1/mentions/{id}` with `done` or `assigneeId`.
- **One request per keyword.** A post matching two keywords comes twice, each with its own `data.id` and `data.keyword`.
## Routing on the payload
| To route by | Use |
| --- | --- |
| Rule | `event`, `alert.id` |
| Keyword | `data.keyword.term`, `data.keyword.id`, `data.keyword.group` |
| Platform | `data.post.platform` |
| Sentiment, intent, relevance | `data.classification.sentiment`, `.intents`, `.relevance` |
| Author | `data.author.followers`, `data.author.tags` |
| Urgency | `data.priority`, the feed's priority score |
See [Webhooks](/webhooks) for retries and signatures.
# Digest events
The summary an hourly, daily or weekly rule sends each period, field by field.
| Mode | Sent | Covers |
| --- | --- | --- |
| `hourly` | 5 minutes past each UTC hour | The previous hour |
| `daily` | `schedule.hour` and `schedule.minute` in `schedule.timezone` | Since the last digest |
| `weekly` | The same, on `schedule.weekday` | Since the last digest |
Daily and weekly rules skip a period without a relevant mention (`schedule.skipEmpty`, on by default). Hourly rules skip an hour with nothing at or above their minimum. `POST /v1/alerts/{id}/run` sends the last period now and keeps the schedule. A mention counts in the period it was scored. A rule can rename `event`, which defaults to `digest`.
```json
{
"id": "dlv_e93f0c6d9c07478faaba938b561e5246",
"event": "digest",
"createdAt": "2026-10-03T10:35:50.309Z",
"alert": { "id": "feed_eaa52968cbfb4f359a147d700418d7ea", "name": "Morning digest" },
"data": {
"window": { "from": "2026-10-02T10:35:50.175Z", "to": "2026-10-03T10:35:50.175Z" },
"matched": 24,
"relevant": 20,
"previousMatched": 20,
"byPlatform": [
{ "platform": "bluesky", "count": 12 },
{ "platform": "devto", "count": 11 },
{ "platform": "hackernews", "count": 1 }
],
"sentiment": { "positive": 4, "neutral": 16, "negative": 0 },
"top": [ { "id": "mm_b84bdf7f7ddf4fc29b3786f9013e89e8", "status": "open", "relevant": true, "...": "..." } ],
"negative": [],
"buyIntent": [ { "...": "..." } ],
"questions": [ { "...": "..." } ]
}
}
```
## Fields
| Field | Meaning |
| --- | --- |
| `window` | The period, in UTC |
| `matched` | Open, scored matches the rule accepts, relevant or not |
| `relevant` | Those with relevance 40 or higher |
| `previousMatched` | `matched` for the previous period |
| `byPlatform` | Matches per platform, largest first |
| `sentiment` | Relevant mentions per sentiment |
| `top` | Up to 10 open mentions, by `priority` |
| `negative` | Up to 5 negative mentions |
| `buyIntent` | Up to 5 with `buy_intent` |
| `questions` | Up to 5 with `question` |
Lists hold full [mentions](/api/mentions/get-mention) at or above the rule's minimum. A mention can be in several lists.
## Delivery
A digest is sent as built, even if you triage its mentions meanwhile. Editing the rule cancels a digest not yet sent.
Each channel retries every 5 minutes for up to 6 hours, hourly digests until 45 minutes past the next hour. The delivery log lists scheduled daily digests as `feed_…:2026-10-04` and hourly ones as `feed_…:h:2026-10-04T09`. See [Webhooks](/webhooks) for signatures.
# Account events
Webhooks for changes to keywords, your balance and things that need attention, which are not tied to a mention.
Some things that happen aren't posts. A keyword reaches its cap, the balance runs out, a keyword starts collecting again, or a spike needs a look. No alert rule sends these. Instead, a channel subscribes to them by name with `events`.
```ts tab="TypeScript"
await rumoro.updateChannel({
path: { id: 'dest_...' },
body: { events: ['keyword.capped', 'keyword.paused_for_balance', 'keyword.resumed', 'wallet.low', 'wallet.paused', 'wallet.resumed'] },
});
```
```python tab="Python"
rumoro.channels.update(
"dest_...",
events=["keyword.capped", "keyword.paused_for_balance", "keyword.resumed", "wallet.low", "wallet.paused", "wallet.resumed"],
)
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/channels/dest_... \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "events": ["keyword.capped", "keyword.paused_for_balance", "keyword.resumed", "wallet.low", "wallet.paused", "wallet.resumed"] }'
```
You can also send `events` on `POST /v1/channels`. On `PATCH` it replaces the whole list. The channel shows its list as `config.events`, which is empty when it subscribes to none. A channel only gets events that happen after it subscribes. Its rules keep working as before.
Slack, email and Telegram channels can subscribe to the four attention events only. Other names return `400`. Instead of JSON, they get a one-line "Needs attention" message with an **Open in Rumoro** link.
## The payload
Account events use the normal envelope with `alert: null`. Check the signature like for any delivery ([signature verification](/webhooks#signature-verification)).
```json
{
"id": "dlv_...",
"event": "keyword.capped",
"createdAt": "2026-10-04T15:20:00.000Z",
"alert": null,
"data": {
"keyword": { "id": "kw_...", "term": "driftwood", "kind": "brand" },
"cap": { "mentions": 750 },
"thisMonth": 750,
"pausedAt": "2026-10-04T15:20:00.000Z",
"resumesAt": "2026-11-01T00:00:00.000Z"
}
}
```
## The events
| Event | Sent when | `data` |
| --- | --- | --- |
| `keyword.capped` | A keyword reached its monthly cap | `keyword`, `cap.mentions`, `thisMonth` (matches in the month of the pause), `pausedAt`, `resumesAt` (the first of next month in UTC, unless you raise or remove the cap before) |
| `keyword.paused_for_balance` | This keyword stopped because the balance ran out | `keyword`, `pausedAt`, `wallet` |
| `keyword.resumed` | The keyword is collecting again | `keyword`, `reason`, `resumedAt` |
| `wallet.low` | The effective balance dropped to 20% of the last credit. Sent once per credit, and not while tracking is paused. | `wallet`, `lastCreditCents`, `at` |
| `wallet.paused` | All keywords stopped for lack of balance | `wallet`, `keywordsPaused`, `at` |
| `wallet.resumed` | A credit covered one day of all keywords | `wallet`, `keywordsResumed`, `at` |
| `mention.spike` | A keyword got far more mentions in the last full hour than usual | `attentionId`, `url`, `keyword`, `window`, `matches`, `relevant`, `baseline` (`meanPerHour`, `stddevPerHour`, `hours`) |
| `sentiment.negative_spike` | A keyword's share of negative mentions over 24 hours jumped compared with the week before | `attentionId`, `url`, `keyword`, `window`, `negative`, `relevant`, `share`, `baseline` (`negative`, `relevant`, `share`) |
| `keyword.noisy` | Most of a keyword's scored matches are noise | `attentionId`, `url`, `keyword`, `windowDays`, `scored`, `relevant`, `noiseShare` |
| `channel.failing` | A channel's last 5 deliveries in 24 hours all failed | `attentionId`, `url`, `channel` (`id`, `kind`, `label`), `failures`, `lastError`, `since` |
When the whole wallet pauses or restarts, you first get one `keyword.paused_for_balance` or `keyword.resumed` per keyword, then `wallet.paused` or `wallet.resumed`.
`reason` on `keyword.resumed` tells you why.
| `reason` | Meaning |
| --- | --- |
| `balance_restored` | A credit brought the balance back |
| `cap_raised` | The cap was raised, including when the first top-up lifts the welcome cap |
| `cap_removed` | The cap was removed |
| `month_turned` | A new month started |
`wallet` is the balance right after the change, with `balanceCents`, `effectiveCents`, `nextDayCents` (one more day of the running keywords), `resumeCostCents` (one day of all keywords), `activeKeywords` and `pausedKeywords`. [`GET /v1/usage`](/api/usage/get-usage) has the current numbers.
Muting or unmuting a keyword yourself sends no event.
**Attention events** (the last four) go out once, as an attention item opens. `attentionId` is that item, and `GET /v1/attention` lists all of them. Their `keyword` also has `name`, which is the term, or "term (Group)" for a keyword outside the default group, and `groupId`. `url` opens the matching page in the dashboard. See [Needs attention](/guides/attention).
## Delivery
An event is recorded at the moment of the change, and the delivery run that follows, every minute, sends it. You get each event at least once, with the [usual retries](/webhooks#retries) and the same `id` on every attempt. In the [delivery log](/api/alerts/list-channel-deliveries) they have `kind: "event"` and the event name. Owners still get their emails about low balance, paused tracking and capped keywords.
# Alerts and channels
Alert rules choose which mentions you hear about and how often. Channels are Slack, Telegram, email or a webhook.
Every alert has a **rule** and sends to one or more **channels**. Its filter decides which mentions count, by keyword, platform, relevance, sentiment, intent, language, links, authors and more. Its mode decides how often. `instant` sends each mention as soon as it is scored. `hourly`, `daily` and `weekly` send one digest per period, at the local time and weekday you choose.
One channel can serve many rules. Alerts and digests cost nothing.
## Channels
| Kind | How to connect | What it gets |
| --- | --- | --- |
| Slack | Connect Slack and choose a channel ([Slack](/alerts/slack)) | Instant messages with **Interacted** and **Mute** buttons, and every kind of digest |
| Telegram | Start the bot or add it to a group ([Telegram](/alerts/telegram)) | Instant messages and every kind of digest |
| Email | Up to 20 addresses | Instant emails, at most 20 an hour per channel, plus daily and weekly digests |
| Webhook | A URL, with a signing secret you see once ([Webhooks](/webhooks)) | Signed JSON for each mention or digest, with the rule's `event` name |
### Attention alerts
Any channel can also get [attention alerts](/guides/attention), without a rule. These are short messages when a keyword spikes, turns negative or gets noisy, or when a channel keeps failing. Turn them on per channel under Alerts, or with `events` on `PATCH /v1/channels/{id}`.
### Email
A workspace created in the dashboard starts with a **Daily digest** rule that emails you at 09:00 your time. To email more addresses, add them in Alerts or in Settings › Integrations › Email. Team members with a verified email are confirmed right away. Anyone else first has to click a confirmation link, which is valid for 24 hours.
After 20 instant emails in an hour, further mentions wait for the channel's **daily** digest (up to 100). If the channel has no daily digest rule, they only stay in the feed.
## What reaches a rule
- A mention is sent when it passes the filter and its relevance is at least `minRelevance` (40 by default, and 0 sends everything).
- The look-back of a new keyword goes to the feed and digests, never to instant alerts.
- People muted for the whole workspace never reach any channel.
- If you mark a mention not relevant, done or ignored before it is sent, it isn't sent.
- In Slack and Telegram, a post matched by several keywords or rules arrives as one message per channel.
## Mute authors
A rule's muted list keeps certain people out of that rule, such as your team, a bot or a competitor. Their mentions still appear in the feed and still count toward usage. A profile link or a link to one of their posts mutes that account.
| You enter | Rumoro saves |
| --- | --- |
| `https://x.com/name`, `https://twitter.com/name/status/123` | `https://x.com/name` |
| `https://www.linkedin.com/in/name`, `…/company/name/posts` | The profile or company page link |
| `https://www.reddit.com/user/name`, `u/name` | `https://www.reddit.com/user/name` |
| `https://news.ycombinator.com/user?id=name` | The same link |
| `name.bsky.social`, `did:plc:…` | The profile link, or the DID |
| `https://www.youtube.com/channel/UC…` | The same link |
| `https://www.tiktok.com/@name`, `…/@name/video/123` | `https://www.tiktok.com/@name` |
| `https://www.instagram.com/name/` | The same link. A post link doesn't name the author. |
| A GitHub, DEV or Stack Overflow profile link | The profile link |
| Any other site, such as `https://techcrunch.com/…` | `https://techcrunch.com`, which mutes that publication |
| `@name`, `Display Name` | `name` or `Display Name`, which mutes that name or handle on every platform |
Links that don't name a person, such as a subreddit, a thread or a story, are refused, and the error names the entry. A YouTube `@handle` link is saved but never matches, because mentions carry the channel link and name, so use one of those. A rule can mute up to 200 authors.
## Watch a link
A rule can react to what a post **links to**, for example a partner's referral link or a competitor's website. Put up to 20 hosts in `filter.linkHosts` ("Links to" in the dashboard). `["linear.app"]` also matches `docs.linear.app`, and a full URL or a `www.` prefix counts as the same host. Posts without links never match. `GET /v1/mentions` and `GET /v1/people` accept the same `linkHosts` filter.
```ts tab="TypeScript"
await rumoro.createAlert({
body: { name: 'Links to our docs', mode: 'instant', filter: { linkHosts: ['docs.example.dev'] }, channelIds: ['dest_...'] },
});
```
```python tab="Python"
rumoro.alerts.create(
name="Links to our docs", mode="instant", filter={"linkHosts": ["docs.example.dev"]}, channelIds=["dest_..."]
)
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/alerts \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Links to our docs", "mode": "instant", "filter": { "linkHosts": ["docs.example.dev"] }, "channelIds": ["dest_..."] }'
```
## One rule, several cases
To catch, say, "negative posts on Hacker News, or any question", list the cases in `filter.anyOf`. A mention is sent when the rest of the filter matches and at least one case does ([Conventions](/conventions#or-across-groups-anyof)).
```ts tab="TypeScript"
await rumoro.createAlert({
body: {
name: 'Needs a reply',
mode: 'instant',
filter: { anyOf: [{ platforms: ['hackernews'], sentiments: ['negative'] }, { intents: ['question'] }] },
channelIds: ['dest_...'],
},
});
```
```python tab="Python"
rumoro.alerts.create(
name="Needs a reply",
mode="instant",
filter={"anyOf": [{"platforms": ["hackernews"], "sentiments": ["negative"]}, {"intents": ["question"]}]},
channelIds=["dest_..."],
)
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/alerts \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Needs a reply", "mode": "instant",
"filter": { "anyOf": [ { "platforms": ["hackernews"], "sentiments": ["negative"] },
{ "intents": ["question"] } ] },
"channelIds": ["dest_..."] }'
```
The relevance minimum applies to every case. The dashboard shows the cases on the rule and keeps them when you edit other settings. A rule can also filter by `groupIds`, author `tags`, `minFollowers`, `minConfidence`, star `ratings` and `notRatings`, and engagement minimums.
## Daily and weekly digests
A daily rule sends one digest per channel at the hour and time zone you choose. Mentions count by the time they were scored. A day without a relevant mention (40 or higher) is skipped unless you turn `skipEmpty` off. A weekly rule works the same way on `schedule.weekday` (0 is Sunday, 6 is Saturday).
What a digest contains depends on the channel.
- **Email, Slack and Telegram** show how many mentions came in and how many are relevant, the change against the previous period, the platforms and the keywords. Then come two lists, "Needs you today" and "Also worth a look", with negative mentions and those showing buying intent, questions, bug reports or complaints, most urgent first. Email shows up to 5 and 7, Slack and Telegram up to 5 and 5.
- **Webhooks** get the JSON described in [Digest events](/webhooks/digest-events).
**Run now** sends the rule's period (the last hour, day or week) immediately and doesn't change the schedule.
## Hourly digests
An `hourly` rule sends five minutes past every UTC hour and covers the hour before. An hour with nothing at or above the minimum sends nothing, except with **Run now**. Hourly rules take no `schedule`, and a `weekday` is refused.
Only Slack, Telegram and webhook channels take hourly digests. A rule with an email channel is refused (`400 hourly_email_unsupported`). A delivery that fails on a rate limit or a 5xx is retried until 45 minutes past the hour after it was due, and then skipped, leaving the mentions in the feed.
## Through the API
### Channels
```ts tab="TypeScript"
// List your channels
const { data: channels } = await rumoro.listChannels();
// Create an email channel
await rumoro.createChannel({ body: { kind: 'email', emails: ['support@example.dev'] } });
```
```python tab="Python"
# List your channels
channels = rumoro.channels.list()
# Create an email channel
rumoro.channels.create(kind="email", emails=["support@example.dev"])
```
```bash tab="curl"
# List your channels
curl https://api.rumoro.dev/v1/channels -H "Authorization: Bearer $RUMORO_API_KEY"
# Create an email channel
curl -X POST https://api.rumoro.dev/v1/channels \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "kind": "email", "emails": ["support@example.dev"] }'
```
A Slack channel needs Slack connected first (`409 slack_not_connected`). Telegram chats are added with `POST /v1/telegram/links`.
### Rules
```ts tab="TypeScript"
// Daily at 08:30 in New York
await rumoro.createAlert({
body: { name: 'Morning summary', mode: 'daily', schedule: { hour: 8, minute: 30, timezone: 'America/New_York' }, channelIds: ['dest_...'] },
});
// Every hour, relevant mentions only
await rumoro.createAlert({ body: { name: 'Hourly pulse', mode: 'hourly', channelIds: ['dest_...'] } });
// Every Friday at 16:00 in Paris, French posts only
await rumoro.createAlert({
body: {
name: 'Weekly recap',
mode: 'weekly',
schedule: { hour: 16, minute: 0, timezone: 'Europe/Paris', weekday: 5 },
filter: { languages: ['fr'] },
channelIds: ['dest_...'],
},
});
```
```python tab="Python"
# Daily at 08:30 in New York
rumoro.alerts.create(
name="Morning summary", mode="daily",
schedule={"hour": 8, "minute": 30, "timezone": "America/New_York"}, channelIds=["dest_..."],
)
# Every hour, relevant mentions only
rumoro.alerts.create(name="Hourly pulse", mode="hourly", channelIds=["dest_..."])
# Every Friday at 16:00 in Paris, French posts only
rumoro.alerts.create(
name="Weekly recap", mode="weekly",
schedule={"hour": 16, "minute": 0, "timezone": "Europe/Paris", "weekday": 5},
filter={"languages": ["fr"]}, channelIds=["dest_..."],
)
```
```bash tab="curl"
# Daily at 08:30 in New York
curl -X POST https://api.rumoro.dev/v1/alerts \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Morning summary", "mode": "daily",
"schedule": { "hour": 8, "minute": 30, "timezone": "America/New_York" }, "channelIds": ["dest_..."] }'
# Every hour, relevant mentions only
curl -X POST https://api.rumoro.dev/v1/alerts \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Hourly pulse", "mode": "hourly", "channelIds": ["dest_..."] }'
# Every Friday at 16:00 in Paris, French posts only
curl -X POST https://api.rumoro.dev/v1/alerts \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Weekly recap", "mode": "weekly",
"schedule": { "hour": 16, "minute": 0, "timezone": "Europe/Paris", "weekday": 5 },
"filter": { "languages": ["fr"] }, "channelIds": ["dest_..."] }'
```
`PATCH /v1/alerts/{id}` replaces the whole filter, including `excludeAuthors`. A rule's name is 1 to 80 characters, and a rule can have up to 20 channels.
### Mute and unmute
These change the muted list without rewriting the filter. Both take the same body, are safe to repeat and return the rule.
```ts tab="TypeScript"
await rumoro.muteAlertAuthors({
path: { id: 'feed_...' },
body: { authors: ['https://x.com/ourcompany', 'u/release-bot', 'name.bsky.social'] },
});
await rumoro.unmuteAlertAuthors({ path: { id: 'feed_...' }, body: { authors: ['name.bsky.social'] } });
```
```python tab="Python"
rumoro.alerts.mute("feed_...", authors=["https://x.com/ourcompany", "u/release-bot", "name.bsky.social"])
rumoro.alerts.unmute("feed_...", authors=["name.bsky.social"])
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/alerts/feed_.../mute \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "authors": ["https://x.com/ourcompany", "u/release-bot", "name.bsky.social"] }'
curl -X POST https://api.rumoro.dev/v1/alerts/feed_.../unmute \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "authors": ["name.bsky.social"] }'
```
### Test and run
```ts tab="TypeScript"
// Send a test through every channel of the rule
await rumoro.testAlert({ path: { id: 'feed_...' } });
// Send the digest for the current period now
await rumoro.runAlertDigest({ path: { id: 'feed_...' } });
```
```python tab="Python"
# Send a test through every channel of the rule
rumoro.alerts.test("feed_...")
# Send the digest for the current period now
rumoro.alerts.run("feed_...")
```
```bash tab="curl"
# Send a test through every channel of the rule
curl -X POST https://api.rumoro.dev/v1/alerts/feed_.../test -H "Authorization: Bearer $RUMORO_API_KEY"
# Send the digest for the current period now
curl -X POST https://api.rumoro.dev/v1/alerts/feed_.../run -H "Authorization: Bearer $RUMORO_API_KEY"
```
# Slack integration
Get relevant mentions in a Slack channel, with buttons to open, close or mute them.
Rumoro's Slack app posts new relevant mentions to a channel you choose. You connect it with a normal Slack install, so there are no tokens to copy. Disconnecting removes the app's access.
## Connect from the dashboard
Go to **Settings › Integrations › Slack notifications**.
1. Click **Connect Slack** and approve the install.
2. **Choose a channel** and a minimum relevance. The options are all relevant, 50 and up (the default), 70 and up, or 85 and up. You can also limit platforms and mute authors.
3. **Turn notifications on.**
This creates an instant rule called "Slack notifications" under **Alerts**. Any other rule can send to Slack too. The list shows public channels, which work without an invite. For a private channel, invite **@Rumoro** to it first.
## What you see in Slack
Each mention is one message with
- a header that says what kind of mention it is, such as `🔵 New mention of driftwood`, `💰 Buying signal for driftwood`, `🐞 Bug report about driftwood` or `🔴 Negative mention of driftwood`
- the platform, keyword, sentiment and intents
- the author and the post, quoted up to 600 characters
- the classifier's note on why it matters
- buttons that stay the same after a click
- **Open on …** opens the post on its platform.
- **Open in Rumoro** opens the feed, filtered to that person or keyword.
- **Interacted** marks the post done, replies in the thread with who clicked, and adds a line such as "Interacted by sam via Slack, 2026-10-04" to the note.
- **Mute …** mutes the author on the rule that sent the message, after you confirm. It only appears when the author has a name and a profile link.
A message stands for the post, however many keywords or rules matched it, so **Interacted** marks every match done. Each click adds its own line, so the team can see that someone already replied.
A digest is one message. It shows the counts against the previous period, the platforms and keywords, up to 5 mentions under "Needs you today" and 5 under "Also worth a look", and a link to the feed.
## Delivery
Only scored mentions at or above the rule's minimum are sent. A mention marked done or ignored first is skipped. Slack rate limits and Slack server errors are retried, up to 5 attempts. Other Slack errors, such as a removed install or a deleted or archived channel, fail right away and show up in the delivery history.
## Connect through the API
The Slack connection endpoints aren't in the API reference. They answer as described under [Outside the reference](/conventions#outside-the-reference).
```bash
# 1. Start the install, then open the returned URL in a browser.
curl -X POST https://api.rumoro.dev/v1/slack/install \
-H "Authorization: Bearer $RUMORO_API_KEY"
# { "url": "https://slack.com/oauth/v2/authorize?..." }
# 2. After approval, check the connection.
curl https://api.rumoro.dev/v1/slack/status -H "Authorization: Bearer $RUMORO_API_KEY"
# { "configured": true, "connected": true, "teamName": "Driftwood", "notifications": null }
# 3. List the channels and turn notifications on for one.
curl https://api.rumoro.dev/v1/slack/channels -H "Authorization: Bearer $RUMORO_API_KEY"
curl -X PUT https://api.rumoro.dev/v1/slack/notifications \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "channelId": "C07NQ4R2TLM", "channelName": "social-mentions", "minRelevance": 60 }'
```
The `PUT` also takes `sources` and `excludeAuthors`. `DELETE /v1/slack/notifications` turns notifications off and leaves Slack connected. `DELETE /v1/slack` disconnects and removes the app's access. Once connected, a Slack channel is a normal [channel](/api/alerts/create-channel) that any rule can use.
## Errors
| Code | Status | When |
| --- | --- | --- |
| `slack_not_configured` | 503 | Slack isn't available |
| `slack_not_connected` | 404 | No Slack workspace is connected, or its access was removed. `POST /v1/channels` returns 409 instead. |
| `upstream_unavailable` | 502 | Slack didn't respond while listing channels |
# Telegram integration
Mention alerts and digests, posted by a Telegram bot to you or your team's group.
**@RumoroAlertsBot** sends instant alerts and digests to a chat with you or to a team group. Connecting takes one tap, without tokens or chat ids.
## Connect from the dashboard
Go to **Settings › Integrations › Telegram**.
1. Click **Connect Telegram** to get a one-time link. It is valid for 15 minutes.
2. Click **Open in Telegram** and press **Start**, or choose **Add to a group**. The chat shows up in the dashboard within seconds.
3. In **Alerts**, pick the rules that should post to this chat.
If `t.me` links don't open, message the bot `/start `, using the code shown next to the buttons.
One chat can serve many rules. Removing a chat detaches it from all of them. Groups that turn into supergroups keep working. The bot only posts to chats that are connected.
## What you see in Telegram
```
🐞 Bug report about driftwood
by nadia_dev
The user reports that flags stop updating after the app goes to the background.
> The text of the post, up to 600 characters.
Open on Reddit · See it in the feed
```
The first line says what kind of mention it is, as in [Slack](/alerts/slack). Then come the author, the classifier's note and the post.
A digest is one message. It shows how many mentions came in and how many are relevant, the change against the previous period, the platforms and keywords, then up to 5 mentions under "Needs you today" and 5 under "Also worth a look", and a link to the feed. If it would be longer than Telegram's limit of 4,096 characters, the last sections are left out.
## Delivery
Only scored mentions that pass the filter are sent, once per post per chat. When Telegram asks the bot to slow down, Rumoro waits and tries again, up to 5 times. For a very busy keyword, a digest works better. A chat that blocked or removed the bot fails right away and shows up in the delivery history.
## Connect through the API
```bash
# 1. Create a connect link.
curl -X POST https://api.rumoro.dev/v1/telegram/links \
-H "Authorization: Bearer $RUMORO_API_KEY"
# { "url": "https://t.me/RumoroAlertsBot?start=...", "groupUrl": "https://t.me/RumoroAlertsBot?startgroup=...",
# "code": "...", "expiresAt": 1759600000000 }
# 2. After Start, the chat is a channel of kind "telegram" (see GET /v1/channels). Point a rule at it.
curl -X POST https://api.rumoro.dev/v1/alerts \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Team chat", "mode": "instant", "filter": { "minRelevance": 60 }, "channelIds": ["dest_..."] }'
```
`GET /v1/telegram/status` returns `{ "configured": true, "botUsername": "RumoroAlertsBot" }`. `DELETE /v1/channels/{id}` disconnects a chat. The Telegram connection endpoints aren't in the API reference.
## Errors
| Code | Status | When |
| --- | --- | --- |
| `telegram_not_configured` | 503 | Telegram isn't available |
| `internal_error` | 502 | Telegram didn't respond while Rumoro looked up the bot |
# Needs attention
Rumoro checks every hour for spikes, negative turns, noisy keywords and failing channels, and tells you where you want.
Some things can't wait for tomorrow's digest. A keyword suddenly gets many times its usual mentions, a day turns negative, a keyword's matches are mostly noise that you still pay for, or a channel stops receiving. Rumoro looks for these every hour and opens an **attention item** for each one.
```ts tab="TypeScript"
const { data: items } = await rumoro.listAttention();
```
```python tab="Python"
items = rumoro.attention.list()
```
```bash tab="curl"
curl https://api.rumoro.dev/v1/attention \
-H "Authorization: Bearer $RUMORO_API_KEY"
```
```json
{
"data": [
{
"id": "att_...",
"kind": "mention.spike",
"status": "open",
"subject": { "type": "keyword", "id": "kw_..." },
"title": "Spike on driftwood: 23 mentions in an hour, usually about 3",
"openedAt": "2026-10-04T15:00:00.000Z",
"resolvedAt": null,
"dismissedAt": null,
"data": {
"attentionId": "att_...",
"url": "https://app.rumoro.dev/w/{workspaceId}/mentions?keywordId=kw_...",
"keyword": { "id": "kw_...", "term": "driftwood", "kind": "brand", "name": "driftwood", "groupId": "grp_..." },
"window": { "from": "2026-10-04T14:00:00.000Z", "to": "2026-10-04T15:00:00.000Z" },
"matches": 23,
"relevant": 17,
"baseline": { "meanPerHour": 2.6, "stddevPerHour": 3.1, "hours": 168 }
}
}
],
"nextCursor": null
}
```
By default you get open items. Set `status` to `resolved`, `dismissed` or `all` for older ones, and `kind` (comma-separated) to narrow the list. A page has 50 items (`limit` up to 100), newest first. Send `nextCursor` back as `cursor` for the next page. `openedAt` is the hour the item was found, the same as `window.to`.
## The four kinds
Rumoro checks once every UTC hour, starting two minutes past, and looks at the hour before.
| Kind | Opens when | Closes when |
| --- | --- | --- |
| `mention.spike` | A keyword's last full hour has at least 10 matches, at least 4 times its hourly average, and at least 4 standard deviations above it. Only posts published within 6 hours before they matched count, so look-backs and slow indexing don't trigger it. The average covers up to the previous week, and a keyword needs 3 days of data first. | The next hour is normal again |
| `sentiment.negative_spike` | In the last 24 hours, at least 5 of at least 10 relevant mentions are negative. That has to be 20% or more, and at least 2.5 times the keyword's usual share. The usual share comes from the week before and is smoothed for small keywords as (negative + 0.8) / (relevant + 10). | The last 24 hours no longer meet this |
| `keyword.noisy` | Its [health](/guides/keyword-health) is `noisy`, meaning 20 or more scored matches in 14 days (or since your last change) with less than 30% relevant | Health shows anything else, or the keyword is muted or deleted |
| `channel.failing` | A channel's last 5 deliveries in 24 hours all failed. Email limits, unconfirmed addresses and channel types that aren't set up on this deployment don't count as failures. | The channel delivers again, or is turned off or deleted |
## One item per episode
An item opens when its condition starts and closes by itself when the condition ends, so a spike that lasts three hours is one item. Each subject has at most one open item per kind, and after an item closes, the same kind doesn't open again for 24 hours. Closed items are listed with `status=resolved`.
```ts tab="TypeScript"
await rumoro.dismissAttention({ path: { id: 'att_...' } });
```
```python tab="Python"
rumoro.attention.dismiss("att_...")
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/attention/att_.../dismiss \
-H "Authorization: Bearer $RUMORO_API_KEY"
```
Dismissing hides an item for as long as its condition lasts. If the condition ends and starts again later, a new item opens.
## Getting notified
When an item opens, Rumoro also sends it once as an [account event](/webhooks/account-events) with the same name.
- **Webhooks** subscribe with `events` on the channel and receive the item's `data`.
- **Slack, email and Telegram** get a short "Needs attention" message with the title and an **Open in Rumoro** link. Turn on **Attention alerts** for a channel under Alerts, or send this.
```ts tab="TypeScript"
await rumoro.updateChannel({
path: { id: 'dest_...' },
body: { events: ['mention.spike', 'sentiment.negative_spike', 'keyword.noisy', 'channel.failing'] },
});
```
```python tab="Python"
rumoro.channels.update("dest_...", events=["mention.spike", "sentiment.negative_spike", "keyword.noisy", "channel.failing"])
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/channels/dest_... \
-H "Authorization: Bearer $RUMORO_API_KEY" -H "Content-Type: application/json" \
-d '{ "events": ["mention.spike", "sentiment.negative_spike", "keyword.noisy", "channel.failing"] }'
```
Email follows the same rules as instant alerts, so only confirmed addresses, and at most 20 an hour per channel. When an item closes, nothing is sent.
The dashboard lists open items above the mentions under **Needs attention**, each with a **Dismiss** button. MCP has `list_attention` and `dismiss_attention`. The CLI has `attention:list` and `attention:dismiss`. Attention items cost nothing.
# Keyword groups
Organise keywords by customer, campaign or product, track the same word for several of them, and see each group's cost.
Groups let one workspace work for several customers, campaigns or products. If two of them need the same word, each gets its own keyword with its own rules, cap and line on the bill. If you only track your own brand, you don't need groups.
A keyword is always in one group. Each workspace starts with a **Default** group, which gets keywords created without a `groupId`. You can rename it but not delete it. A term can appear once per group (case doesn't matter), so two groups can each have their own `atlas` keyword.
## Create a group and add keywords
`name` is required, 1 to 80 characters and unique. `externalId` (up to 128 characters) is optional and also unique. Use it for your own id, such as a customer id, and find the group again with `GET /v1/groups?externalId=`.
```ts tab="TypeScript"
const { data: group } = await rumoro.createGroup({ body: { name: 'Northwind Bikes', externalId: 'acct_4471' } });
```
```python tab="Python"
group = rumoro.groups.create(name="Northwind Bikes", externalId="acct_4471")
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/groups \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Northwind Bikes", "externalId": "acct_4471" }'
```
```json
{
"id": "grp_...",
"name": "Northwind Bikes",
"externalId": "acct_4471",
"isDefault": false,
"context": null,
"stats": { "keywords": 0, "active": 0 },
"createdAt": "2026-10-04T10:41:09.317Z",
"updatedAt": "2026-10-04T10:41:09.317Z"
}
```
```ts tab="TypeScript"
await rumoro.createKeyword({ body: { term: 'atlas', kind: 'brand', groupId: 'grp_...', cap: { mentions: 400 } } });
```
```python tab="Python"
rumoro.keywords.create(term="atlas", kind="brand", groupId="grp_...", cap={"mentions": 400})
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/keywords \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "term": "atlas", "kind": "brand", "groupId": "grp_...", "cap": { "mentions": 400 } }'
```
A second `atlas` in the same group returns `409 duplicate_keyword`. To move a keyword, send a new `groupId` with `PATCH /v1/keywords/{id}`. You get the same 409 if the target group already has that term. A group's `stats` count its `keywords` and how many of them are `active`.
## A company description per group
The classifier scores mentions against your workspace's company profile. When the workspace serves several businesses, give each group its own `context` of up to 4,000 characters. Say who the business is, what it sells, to whom, and what it is not.
For that group's keywords, this text replaces the whole workspace profile. So rules that should apply to the whole group, such as "ignore job ads", belong here too. Keyword `context` stays a short note for one term. Set the group's `context` to `null` to go back to the workspace profile. The default group has no `context` of its own (`400 default_group_context`).
```ts tab="TypeScript"
await rumoro.updateGroup({
path: { id: 'grp_...' },
body: {
context: 'Northwind Bikes builds cargo e-bikes in Utrecht and sells them online and through dealers. Atlas is their family cargo bike. Not the moving company, not the database.',
},
});
```
```python tab="Python"
rumoro.groups.update(
"grp_...",
context="Northwind Bikes builds cargo e-bikes in Utrecht and sells them online and through dealers. Atlas is their family cargo bike. Not the moving company, not the database.",
)
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/groups/grp_... \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "context": "Northwind Bikes builds cargo e-bikes in Utrecht and sells them online and through dealers. Atlas is their family cargo bike. Not the moving company, not the database." }'
```
Matches stored after the change use the new text. Earlier ones keep the old text, even if they aren't scored yet. Nothing is scored again.
## Where the group shows up
- Keywords have a `group` object (`id`, `name`, `externalId`, `isDefault`). `GET /v1/keywords?groupId=` takes one id or a comma-separated list.
- Mentions have `keyword.group`, in the API and in webhooks. `groupIds` and `notGroupIds` filter `GET /v1/mentions` and the exports, and the CSV has `group` and `group_external_id` columns.
- Alert rules take `groupIds`, so you can have one rule per customer that sends to that customer's channel.
- Each keyword row in the analytics reports carries its group.
- In emails and digests, a keyword outside the default group appears as `atlas (Northwind Bikes)`.
## What a group cost
`GET /v1/usage/breakdown?by=group` shows each group's keyword-days, matched and billed mentions, and total at list price for a month.
```ts tab="TypeScript"
const { data: breakdown } = await rumoro.getUsageBreakdown({ query: { by: 'group', month: '2026-10' } });
```
```python tab="Python"
breakdown = rumoro.usage.breakdown(by="group", month="2026-10")
```
```bash tab="curl"
curl "https://api.rumoro.dev/v1/usage/breakdown?by=group&month=2026-10" \
-H "Authorization: Bearer $RUMORO_API_KEY"
```
Keyword-days count for the group the keyword was in on that day. Mention charges count for its current group, and those of a deleted keyword show under `totals.unattributedBillable`. A group deleted during the month keeps its row, named "Deleted group" with `group.removed: true`.
## Deleting a group
`DELETE /v1/groups/{id}` returns 204 and deletes the group with its keywords and their mentions. Past charges stay on record. Check `stats.keywords` first if you want to know how many keywords will go. A rule that only named this group is turned off rather than widened, and a view that names it no longer matches anything.
## In the dashboard
The **Keywords** page has a **Groups** button and a group picker. As soon as a second group exists, each keyword shows its group, and the keyword editor gets a group field.
# Keyword health
See which keywords cost more than they're worth, where their noise comes from, and fix them with one PATCH.
You pay for every matched mention, relevant or not. The health report tells you whether a keyword is worth what it costs, where its noise comes from, and which change would reduce it, with an estimate of what that change would have saved.
```ts tab="TypeScript"
const { data: health } = await rumoro.getKeywordHealth({ path: { id: 'kw_...' }, query: { range: '30d' } });
```
```python tab="Python"
health = rumoro.keywords.health("kw_...", range_="30d")
```
```bash tab="curl"
curl "https://api.rumoro.dev/v1/keywords/kw_.../health?range=30d" \
-H "Authorization: Bearer $RUMORO_API_KEY"
```
`range` is `7d`, `30d` (the default) or `90d`. It counts UTC days up to today, by the time of the match. Reading the report changes nothing and costs nothing.
## Status
The first status that applies is used.
| Status | When |
| --- | --- |
| `paused` | Muted by you, stopped by an empty balance, or stopped by the noise brake |
| `capped` | At its monthly mention cap, including the 200 of the welcome credit. It matches again on the 1st (UTC) or when the cap goes up. |
| `noisy` | 20 or more scored matches, of which less than 30% are relevant (relevance 40 or higher, or marked relevant) |
| `new` | Younger than 7 days and not noisy, or changed in the last 7 days with fewer than 20 scored matches since |
| `quiet` | At least 7 days old, with no relevant match in the period |
| `healthy` | None of the above |
`reasons` explains the status in plain words. It also points out a platform where 90% or more of at least 10 scored matches were noise, and a keyword that takes half or more of the workspace's matches.
In `GET /v1/keywords`, each keyword has `stats.health`. It uses the same rules over 14 days, so it can differ from a 30-day report.
## Judged since your last change
When you change a keyword's matching rules, platforms or context, or unmute it, the status, `reasons`, noise analysis and suggestions only look at matches since that change, if it falls in the period. `stats.judgedSince` shows when that was, and `null` means the whole period counts.
Suggestions need 20 scored matches in that time. After a change they wait for 20 new ones, and `reasons` says so. A report never suggests again what you just applied.
## Numbers
`stats` always covers the whole period. It has `matches`, `relevant`, `filtered` (noise), `unscored`, `noiseShare`, `workspaceShare`, `byPlatform`, `weekly` (per 7 days) and `cost`, which matches the keyword's row in [`GET /v1/usage/breakdown`](/billing).
## Where the noise comes from
The report takes the newest scored posts of the period, up to 200 noise and 200 relevant ones, under the keyword's current rules and platforms. Reviews are left out, because they match by app and not by text.
- `noiseTerms` lists up to 10 words or two-word phrases that appear in at least 3 noise posts and are at least twice as common there (`noisePosts`, `relevantPosts`, `lift`). Words from the keyword itself never appear.
- `noiseAuthors` lists people with at least 3 noise posts and no relevant one, with the `entry` that `matching.excludedAuthors` would save. When a link doesn't point to one person, such as a GitHub app, the entry is their name.
## Suggestions
Each suggestion has a `patch` you can send to `PATCH /v1/keywords/{id}` unchanged. Lists in it are complete, so your existing entries are included.
### Types
| `type` | What it does |
| --- | --- |
| `platforms` | Removes platforms where 90% or more of at least 10 scored matches were noise, but never all of them. A keyword that tracked every platform gets an explicit list, so platforms added later are not included. |
| `excluded_terms` | Adds the fewest noise terms that cover the noise, which together appear in at most 2% of relevant posts. It needs at least 20 relevant posts, or none at all. |
| `excluded_authors` | Adds the authors who only brought noise |
| `required_terms` | For a keyword without required terms, one word that appears in 90% or more of its relevant posts and removes at least half of the noise. Sets `requiredMode` to `any`. |
| `context` | A new context for the classifier. It changes how new matches are scored, not what is matched or billed. Without `ai=true` it is only offered for `noisy` keywords, as the current context plus "Ignore posts about …" with the three biggest noise terms. |
### Effect
Every suggestion except `context` has an `effect`. Rumoro runs the new rules over the sample and scales the result up to the whole period when the sample is smaller (`exact` tells you which). `centsSaved` is the removed matches at $0.008 each.
```json
{
"type": "excluded_terms",
"values": ["moving", "database"],
"why": "Exclude \"moving\", \"database\": common in its noise, absent from its relevant matches. In the last 30 days it would have removed 38 noise matches and 0 relevant ones.",
"patch": { "matching": { "excludedTerms": ["moving", "database"] } },
"effect": { "noiseRemoved": 38, "relevantRemoved": 0, "centsSaved": 30, "sample": { "noiseRemoved": 38, "noise": 41, "relevantRemoved": 0, "relevant": 26 }, "exact": true },
"source": "rules"
}
```
### Apply one
Send the `patch` as it is.
```ts tab="TypeScript"
await rumoro.updateKeyword({ path: { id: 'kw_...' }, body: { matching: { excludedTerms: ['moving', 'database'] } } });
```
```python tab="Python"
rumoro.keywords.update("kw_...", matching={"excludedTerms": ["moving", "database"]})
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/keywords/kw_... \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "matching": { "excludedTerms": ["moving", "database"] } }'
```
New rules apply from the next check, and mentions you already have stay. A keyword stopped by the noise brake starts again when its required or excluded terms, platforms or context change, as long as the balance covers another day. Excluding authors alone doesn't restart it, so send `"muted": false` as well.
## A context written by a model
With `ai=true`, a language model writes the context from the term, the current context and sample posts. `ai.status` tells you what happened.
| `ai.status` | Meaning |
| --- | --- |
| `off` | Not requested |
| `generated` | Written just now |
| `cached` | The same keyword and period, unchanged, within the last 24 hours |
| `unavailable` | No noise to learn from, or no usable answer. A `noisy` keyword gets the rule-based context instead. |
| `rate_limited` | More than 20 model calls in the last hour for this workspace |
## Limits
Reports are kept for 5 minutes and model contexts for 24 hours. Changing the keyword starts fresh. A workspace can read 30 reports a minute through the API, MCP and dashboard together, cached ones included. After that it gets `429 rate_limited` with `Retry-After`.
## Elsewhere
The Keywords list marks `noisy` and `quiet` keywords, and each keyword's **Health, last 30 days** section has an **Apply** button for every suggestion. In the CLI, run `rumoro keywords:health kw_... --range 30d`. In MCP, use `get_keyword_health` and send patches with `update_keyword`.
# Views
Save a mention filter under a name. Switch to it in the dashboard, read it with viewId, or export it as CSV or JSON.
A view is a saved filter for your mentions with a name, such as "Negative about us" or "Questions from big accounts". Switch between views on the Mentions page, or send a view's id as `viewId` to the API. Views don't store mentions. A view shows whatever matches its filter at the moment you open it.
## The filter
A view's `filter` takes the same fields as `GET /v1/mentions`.
- `keywordIds`, `keywordKinds` (`brand`, `competitor`, `topic`) and `groupIds` ([groups](/guides/groups))
- `platforms`, `languages`, `linkHosts`, `ratings` and `isReply`
- `relevant`, `minRelevance`, `minConfidence`, `sentiments`, `intents` and `automated`
- `minFollowers`, `maxFollowers`, `tags` and `excludeAuthors`
- `minLikes`, `minReposts`, `minReplies`, `minQuotes`, `minViews` and `minBookmarks`
- `status`, and `q`, which searches the post text, title, matched term and author handle
A list matches any of its values. The `not` lists, such as `notPlatforms`, `notGroupIds`, `notIntents` or `notTags`, exclude all of theirs. All conditions must hold, and an empty filter matches every mention. To combine conditions with OR, use `anyOf` with 1 to 10 groups ([Conventions](/conventions#or-across-groups-anyof)). A view has no time window, and no person, assignee or snooze setting.
`name` is 1 to 80 characters and unique, ignoring case (`409 duplicate_view`). `description` can be up to 500 characters.
```ts tab="TypeScript"
const { data: view } = await rumoro.createView({
body: {
name: 'Questions from big accounts',
description: 'Reply within the hour.',
filter: { intents: ['question'], minFollowers: 1000, status: 'open' },
},
});
```
```python tab="Python"
view = rumoro.views.create(
name="Questions from big accounts",
description="Reply within the hour.",
filter={"intents": ["question"], "minFollowers": 1000, "status": "open"},
)
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/views \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Questions from big accounts",
"description": "Reply within the hour.",
"filter": { "intents": ["question"], "minFollowers": 1000, "status": "open" }
}'
```
## Reading a view
`viewId` works together with all other parameters, so adding `since` gives you one period. It also works on `export.csv` and `export.json`. An unknown id returns 404.
```ts tab="TypeScript"
const { data: page } = await rumoro.searchMentions({ query: { viewId: 'vw_...', since: '2026-10-01T00:00:00Z' } });
```
```python tab="Python"
page = rumoro.mentions.search(view_id="vw_...", since="2026-10-01T00:00:00Z")
```
```bash tab="curl"
curl "https://api.rumoro.dev/v1/mentions?viewId=vw_...&since=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer $RUMORO_API_KEY"
```
## On the Mentions page
The view switcher is the first control in the toolbar, and filter chips narrow the view you're on. **Create view** appears once you've set a filter or search. **Edit** opens a view with its own filter chips, and **Manage views** lets you edit or delete any view. `anyOf` groups set through the API stay when you edit in the dashboard.
## Views and alerts
A view is for reading, an alert is for being notified. You can read an alert rule's filter the same way with `alertId`. To turn a view into an alert, send its filter to `POST /v1/alerts`. A field that alerts don't support returns 400.
# Conventions
Paths, ids, methods, paging, filters, errors and versions. The patterns that hold across the whole Rumoro API.
The API is at `https://api.rumoro.dev/v1`. Every resource in the [reference](/api) follows the patterns on this page.
## Resources and paths
Paths use plural nouns and one id, such as `/v1/mentions/{id}`, `/v1/keywords/{id}` or `/v1/alerts/{id}`. An id is a prefix followed by 32 hex characters.
| Prefix | Resource |
| --- | --- |
| `mm_` | mention |
| `aut_` | person |
| `kw_` | keyword |
| `grp_` | group |
| `vw_` | view |
| `seg_` | segment |
| `feed_` | alert rule |
| `dest_` | channel |
| `dlv_` | delivery |
| `att_` | attention item |
| `key_` | API key |
| `org_` | workspace |
An id from another workspace, or one that isn't valid, returns 404 and never 403.
Actions that aren't simple updates are `POST` requests on the resource, such as `/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` and `/v1/attention/{id}/dismiss`.
Analytics come as fixed reports, `/v1/analytics/summary`, `series`, `breakdown`, `share-of-voice` and `reviews`. Each takes `range` (or `from` and `to`), `keywordIds`, `platforms`, `timezone` and `compare`, and returns its `window`. `series` and `breakdown` also take a `by` dimension.
Exports accept the same filters as their list. `/v1/mentions/export.csv` and `export.json` return up to 10,000 rows, `/v1/people/export.csv` up to 5,000.
## Methods and status codes
| Method | Use | Success |
| --- | --- | --- |
| `GET` | Read one item or a list | 200 |
| `POST` on a collection | Create | 201 and the new resource |
| `PATCH` | Change some fields. Fields you leave out stay as they are, `null` clears one. | 200 and the resource |
| `DELETE` | Delete | 204, no body |
| `POST` action | Run an action | 200 and the result |
There are two exceptions. `POST /v1/members/invitations` returns 200 with the existing invitation if the address already has one, and `POST /v1/billing/top-ups` returns 200.
To change anything, use `PATCH`. For a mention, that includes ignoring, closing, assigning, snoozing and adding notes, all through `PATCH /v1/mentions/{id}`.
## Envelopes
A single resource is returned as is. A list is wrapped in `data` with paging information.
```json
{ "data": [ ... ], "nextCursor": "eyJ..." }
```
On the last page `nextCursor` is `null`. For the next page, send it back as `cursor` with the same filters and sort. The cursor remembers the position in the sort order, not your filters.
People and keywords use `limit` and `offset` instead, and return a `total`.
```json
{ "data": [ ... ], "total": 418 }
```
Alerts, channels, views, segments, members and API keys come back as one full list.
## Shapes
Related fields sit in groups, such as `post`, `author`, `classification` and `triage` on a mention, or `reach`, `profile`, `stats` and `annotations` on a person. A missing group is `null`, for example `classification: null` before scoring.
Numbers Rumoro calculates are under `stats`. What you and your team add is under `triage` or `annotations`. A mention shows its `status` (`open`, `ignored`, `done`), `relevant` and `delivered`, never Rumoro's internal processing steps.
Times are ISO 8601 in UTC, like `"2026-10-04T08:21:37.512Z"`. Inputs such as `since`, `until` and `snoozedUntil` also accept epoch milliseconds.
Enum values are lowercase strings. The platforms are `bluesky`, `hackernews`, `github`, `stackoverflow`, `devto`, `reddit`, `x`, `youtube`, `news`, `linkedin`, `tiktok` and `instagram`, plus the review sites `appstore`, `googleplay`, `trustpilot` and `googlemaps`. A keyword's `platforms` lists where its term is searched (`[]` means nowhere) and never contains a review site. Reviews come from its `reviewSources` ([Reviews](/platforms/reviews)).
## Requests
Send JSON bodies with `Content-Type: application/json`, or you get `415 unsupported_media_type`. A body can be up to 128 KiB (`413 payload_too_large`) and must arrive within 5 seconds (`408 request_timeout`).
Sending a create request twice creates two resources, so check before you retry. Delivery and digest actions outside the reference take a `requestId` (a UUID) in the body. Repeating it returns the first answer, and reusing it with a different body returns `409 request_conflict`.
## Query parameters
Names are camelCase and booleans are `true` or `false`. A list can be repeated or comma-separated, so `?tags=vip,customer` is the same as `?tags=vip&tags=customer`. Parameters you leave out don't filter anything, and unknown parameters are ignored.
Most filters come in three forms. The single form (`platform=x`), the list form, which matches any value (`platforms=x,bluesky`), and the `not` form, which excludes all of them (`notPlatforms=news`). The single and list forms can be combined. Different filters narrow each other, so `platforms=x,bluesky¬Sentiments=negative` means X or Bluesky, and nothing negative.
Mentions accept `platforms`, `sentiments`, `intents`, `keywordIds`, `tags`, `languages`, `ratings` and `linkHosts`, each with a `not` form, plus `keywordKinds`, `groupIds` and `notGroupIds` ([groups](/guides/groups)). People accept `platforms`, `tags` and `intents` the same way, plus `keywordKinds` and `neverKeywordKinds`.
A list filter takes up to 50 values. `tags` and `linkHosts` take up to 20. More than that returns `400 validation_error`.
### OR across groups (anyOf)
To combine conditions with OR, give mentions an `anyOf` list of 1 to 10 groups. Each group can hold the same conditions as a [saved view](/guides/views). A mention passes when at least one group matches, and your other filters still apply. Groups can't contain another `anyOf`, a time window or paging. An empty group, an empty `anyOf` or an unknown field returns `400`.
On `GET /v1/mentions` and the exports, send `anyOf` as URL-encoded JSON. This finds negative Reddit posts or questions since September 27.
```ts tab="TypeScript"
const { data: page } = await rumoro.searchMentions({
query: {
since: '2026-09-27T00:00:00Z',
anyOf: JSON.stringify([{ platforms: ['reddit'], sentiments: ['negative'] }, { intents: ['question'] }]),
},
});
```
```python tab="Python"
import json
page = rumoro.mentions.search(
since="2026-09-27T00:00:00Z",
any_of=json.dumps([{"platforms": ["reddit"], "sentiments": ["negative"]}, {"intents": ["question"]}]),
)
```
```bash tab="curl"
curl -G https://api.rumoro.dev/v1/mentions \
-H "Authorization: Bearer $RUMORO_API_KEY" \
--data-urlencode 'since=2026-09-27T00:00:00Z' \
--data-urlencode 'anyOf=[{"platforms":["reddit"],"sentiments":["negative"]},{"intents":["question"]}]'
```
Saved views, alert rule filters and the MCP `search_mentions` tool take the same groups as plain JSON. This rule sends English or German mentions that show buying intent from accounts with 5,000 or more followers, or that are 1 star reviews.
```json
{
"name": "Hot leads",
"mode": "instant",
"filter": {
"languages": ["en", "de"],
"anyOf": [
{ "intents": ["buy_intent"], "minFollowers": 5000 },
{ "ratings": [1] }
]
}
}
```
## Errors
Every error uses the same envelope with a stable code. See [Errors](/errors).
```json
{ "error": { "code": "not_found", "message": "Mention not found", "requestId": "4b0e7d21-..." } }
```
API responses carry an `X-Request-Id` header, and errors repeat it as `error.requestId`. Include it when you contact support. You can send your own `X-Request-Id` of 8 to 64 characters (`A-Z`, `a-z`, `0-9`, `.`, `_`, `-`) and it is returned unchanged. Other values are replaced.
## Keys and scopes
Send `Authorization: Bearer ` with an API key or OAuth token. A `read` credential can only make `GET` requests. Each credential works in one workspace, and `GET /v1/whoami` tells you which. See [Authentication](/authentication).
## Rate limits
Each workspace can make 600 requests a minute. Responses carry `X-RateLimit-*` headers, and requests over the limit get `429 rate_limited`. See [Rate limits](/rate-limits).
## Webhooks
Every event has the same envelope.
```json
{
"id": "dlv_...",
"event": "mention.matched",
"createdAt": "2026-10-04T08:21:37.512Z",
"alert": { "id": "feed_...", "name": "Pricing questions" },
"data": { }
}
```
`data` is the resource as the API returns it. `X-Mentions-Signature-V2` is `v2=` followed by the hex HMAC-SHA256 of `.`, signed with your channel secret. Because the timestamp is signed, an old request can't be sent again. `X-Mentions-Signature` signs only the body and is kept for older code. See [Webhooks](/webhooks).
## Versions
All paths start with `/v1`. New endpoints, optional fields, filters and enum values are added to `/v1` without notice, so ignore anything you don't recognize.
Changes that would break existing code come as a new version, `/v2`. Once `/v2` is out, `/v1` keeps working for at least six months. Endpoints that are going away will send `Deprecation` and `Sunset` headers, and the [changelog](/changelog) will say what changed and how to move. Clients generated from the OpenAPI document follow the new version when you regenerate them.
## Outside the reference
The dashboard's own endpoints for sign-in, setup and billing are not documented. A few endpoints you can call with a key have their own pages. These are connecting [Slack](/alerts/slack) and [Telegram](/alerts/telegram), and retrying or replaying deliveries ([Webhooks](/webhooks)). They wrap a single resource in `{ "data": ... }`, page with `{ "data", "meta": { "nextCursor" } }` and `after`, and return `202` when they queue work.
# Errors
The error envelope and the stable error codes, with what each one means and what to do.
All errors look the same.
```json
{
"error": {
"code": "not_found",
"message": "Mention not found",
"requestId": "4b0e7d21-9c3a-4f62-8d15-7e2a0c9b6f13"
}
}
```
- Handle errors by `code`. Codes don't change, but new ones can appear, so treat an unknown code like any error with its status.
- `message` is for people and can change. For `validation_error` it names the field (`"term: Required"`).
- `requestId` equals the `X-Request-Id` header. Include it when you contact support.
## Retrying
- Retry `5xx`, `408` and `429` after the wait the response gives. Every `429 rate_limited` has `Retry-After` and `retryAfterSeconds`.
- Don't retry `503 database_not_ready`. The deployment needs maintenance.
- Other `4xx` codes mean the request must change. `409 classification_pending` clears once the mention is scored.
## Codes
### Credentials and access
| Code | Status | Meaning |
| --- | --- | --- |
| `unauthorized` | 401 | Missing, unknown, revoked or expired credential. Send `Authorization: Bearer ref_…` with a key from the **API keys** page. |
| `read_only_key` | 403 | A `read` credential tried to write. Use a `write` key. |
| `forbidden` | 403 | Not allowed for this credential or role, such as an API key changing the team or anyone changing an owner's role |
| `key_limit` | 409 | The workspace has 100 keys that aren't revoked |
### Requests
| Code | Status | Meaning |
| --- | --- | --- |
| `validation_error` | 400 | A field or parameter is missing or wrong. The message names it. |
| `not_found` | 404 | No such resource in your workspace, or no such route. Also a cursor whose item is gone. |
| `invalid_cursor` | 400 | The cursor can't be read or belongs to another sort |
| `unsupported_media_type` | 415 | The body isn't `application/json` |
| `payload_too_large` | 413 | The body is over 128 KiB |
| `request_timeout` | 408 | The body took over 5 seconds |
| `rate_limited` | 429 | Over the workspace or endpoint limit ([Rate limits](/rate-limits)) |
| `conflict` | 409 | The change clashes with existing data |
| `request_conflict` | 409 | A `requestId` reused with a different body |
| `stale_version` | 409 | The resource changed since you read it (`ifVersion`) |
### Keywords, groups and billing
| Code | Status | Meaning |
| --- | --- | --- |
| `duplicate_keyword` | 409 | This group already has that term |
| `insufficient_balance` | 402 | The balance can't pay another keyword-day. Top up. |
| `keyword_limit_reached` | 402 | The 500-keyword limit is reached |
| `duplicate_group` | 409 | A group with that name or `externalId` exists |
| `default_group` | 409 | The default group can't be deleted |
| `default_group_context` | 400 | The default group has no description of its own. Use `PATCH /v1/company`. |
| `billing_not_configured` | 503 | Top-ups and invoices aren't available here |
| `no_billing_owner` | 404 | The workspace has no owner to bill |
### Mentions, views, segments and the team
| Code | Status | Meaning |
| --- | --- | --- |
| `invalid_assignee` | 400 | The assignee isn't in the workspace |
| `classification_pending` | 409 | A verdict for a mention that isn't scored yet |
| `already_classified` | 409 | A rescore for a mention that is already scored |
| `keyword_inactive` | 409 | A rescore for a mention whose keyword is muted |
| `duplicate_view` | 409 | A view with that name exists (case ignored) |
| `duplicate_segment` | 409 | A segment with that name exists |
| `already_member` | 409 | The invited address is already a member |
| `last_owner` | 409 | The last owner can't be removed |
### Alerts and channels
| Code | Status | Meaning |
| --- | --- | --- |
| `unknown_channel` | 400 | The rule names a channel outside the workspace |
| `not_a_digest` | 400 | `run` on an instant rule |
| `hourly_email_unsupported` | 400 | Hourly rules can't send email |
| `rule_disabled`, `no_channels` | 409 | The rule is off or has no channel. Only from test and run endpoints outside the reference. |
| `channel_disabled` | 409 | The channel is off |
| `slack_not_connected` | 409 | Slack isn't connected (404 on the Slack connection endpoints) |
| `slack_not_configured`, `telegram_not_configured`, `email_not_configured` | 503 | That channel type isn't set up here |
| `delivery_not_configured`, `delivery_key_unavailable` | 503 | Stored channel secrets can't be used here |
| `no_recipients` | 409 | The email channel has no recipients |
| `recipient_suppressed` | 409 | The address bounced or reported spam |
| `confirmation_rate_limited` | 429 | Too many confirmation emails to this address |
| `version_conflict` | 409 | The webhook's preset rules changed at the same time. Read it again. |
| `invalid_signature` | 401 | An incoming Slack or Telegram request wasn't signed correctly |
| `invalid_token` | 400 | An email link was changed, is malformed or expired (404 for invitation links) |
### Delivery log
| Code | Status | Meaning |
| --- | --- | --- |
| `not_retryable` | 409 | Only failed or held deliveries can be retried |
| `not_replayable` | 409 | Only finished deliveries can be replayed |
| `digest_retry_expired` | 409 | The retry window is over (6 hours, or 45 minutes past the hour for hourly digests) |
| `digest_has_replay` | 409 | There is a newer replay of this digest |
| `payload_too_large` | 409 | The payload is over 1 MiB and can't be sent |
| `email_hourly_limit` | 409 | Retrying an email held by the channel's 20-an-hour limit. It waits for a daily digest rule on the channel. |
| `email_idempotency_expired` | 409 | The safe retry window passed. A replay may send the email twice. |
| `confirmation_requires_resend` | 409 | Confirmation emails are resent, not retried |
| `delivery_request_incomplete` | 500 | A test's saved result couldn't be read. Read it again. |
A test email over the limit of 5 an hour, or an instant email over 20 an hour, is not an HTTP error. The request returns 200 and the delivery shows `email_test_rate_limited` or `email_hourly_limit`.
### Upstream and service
| Code | Status | Meaning |
| --- | --- | --- |
| `upstream_unavailable` | 502 | A provider such as payments or Slack didn't respond. Retry with backoff. |
| `source_unavailable` | 502, 503 | A platform didn't respond to a live search |
| `source_timeout` | 504 | A platform was too slow on a live search |
| `email_transport_unavailable` | 503 | Email sending isn't set up for this delivery |
| `invalid_host`, `invalid_origin` | 403 | An MCP request from a host or origin that isn't allowed |
| `database_unavailable` | 503 | A short database problem. Retry. |
| `database_not_ready` | 503 | The deployment needs maintenance |
| `not_configured` | 503 | No database is configured |
| `internal_error` | 500 | Something failed on our side. Retry, and send the `requestId` if it persists. |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) lists each operation's codes. The [MCP server](/mcp) returns the same codes as a tool error, `{ "error": { "code", "message", "requestId" } }`.
# Rate limits
Each workspace can make 600 requests a minute. How the limit is counted, which headers show it, and how to handle a 429.
Each workspace can make 600 requests a minute, counted over the last 60 seconds. All its API keys and OAuth tokens share this, and [MCP](/mcp) requests count too. The dashboard doesn't.
Counted responses have three headers.
| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed per minute (600) |
| `X-RateLimit-Remaining` | Requests left right now |
| `X-RateLimit-Reset` | When the next request frees up, in Unix seconds |
Over the limit you get `429 rate_limited` with a `Retry-After` header and `retryAfterSeconds` in the body. Nothing is changed and the request isn't counted.
```json
{
"error": {
"code": "rate_limited",
"message": "This workspace made more than 600 requests in a minute. Wait 8 seconds and send the same request again; nothing was applied.",
"requestId": "9e4c2a1f-6d07-4b38-a5c2-0f81d3e7b264",
"retryAfterSeconds": 8
}
}
```
## Endpoint limits
| Endpoint | Limit per workspace |
| --- | --- |
| Mention exports (CSV and JSON together) | 6 a minute, up to 10,000 rows |
| People export | 6 a minute, up to 5,000 rows |
| Usage breakdown | 30 a minute, dashboard not counted |
| Keyword health | 30 a minute, dashboard counted |
| Keyword health with `ai=true` | 20 model calls an hour |
| Top-up checkouts | 5 a minute |
| Invitations | 50 a day |
| Test emails | 5 per channel an hour |
Most return `429 rate_limited` with the same wait fields. Two don't. Over the `ai=true` limit, the health report still returns 200, with AI status `rate_limited` and no model context. Over the test email limit, the test returns 200 and the email shows `email_test_rate_limited`.
## Handling a 429
Wait `Retry-After` seconds, then send the same request again. The [SDKs](/sdks) and [CLI](/cli) don't retry for you. Spreading a big job out works better than retrying. 600 a minute is ten a second, enough to page through a busy workspace.
The limit is per workspace, so two scripts with different keys share it. Dashboard use doesn't count, so a busy script can't lock you out of the app.
# Billing
How Rumoro's prepaid balance works, what keywords and mentions cost, and how to read your wallet and ledger through the API.
You pay for what you use from a prepaid balance. That means the keywords you track and the mentions Rumoro finds for them. There are no seats, plans, subscriptions or bundles. You add money in any amount, and the balance goes down a little every day.
All amounts in the API are whole US cents, so `500` means $5.00.
## Prices
| What | Price | How it is charged |
| --- | --- | --- |
| Keywords | $5 per keyword per month | Charged per day at $5/30. A keyword tracked for 12 days costs 12/30 of a month. |
| Mentions | $0.008 per matched mention | Every match counts, relevant or not. Keywords don't include any mentions. |
A mention is charged once it matches a keyword and has been scored, whether it turns out relevant or noise. Mentions that fail scoring are free, and so are the reviews a new review page brings in.
Charges are settled once a day, shortly after midnight UTC. Until then new matches show as `pendingCents`. The pause rule and the keyword check use `balanceCents - pendingCents`.
[`GET /v1/usage`](/api/usage/get-usage) gives you the overview in one call. It returns the balance (ledger, pending and effective), what a day costs and how many days are left, how many keywords run or are paused, matches today and over 30 days, and whether tracking is paused or the balance is low.
## Cost per keyword
[`GET /v1/usage/breakdown`](/api/usage/get-usage-breakdown) shows usage and cost over a period, in cents at list price, split one of four ways.
- `by=keyword` (the default) gives each keyword's `keywordDays`, `keywordCents`, `billableMentions`, `mentionCents` and `totalCents`. A keyword deleted during the period keeps its row with `keyword.removed: true`, and its mention counts show 0.
- `by=group` gives a row for each [group](/guides/groups), so you can bill per customer or campaign. Keyword-days count under the group the keyword was in on that day. A group deleted during the period keeps its row with `group.removed: true`.
- `by=platform` gives mentions per platform. `keywordDays` is null here.
- `by=day` gives one row per UTC day, oldest first.
Choose the period with `range=7d|30d|90d` (the default is `30d`) or `month=2026-10` for a calendar month. A month in the future returns `400`.
`totals` sums up the whole period. It has keyword-days, matched, billed and unscored mentions, the total at list price, `unattributedBillable` (billed mentions of deleted keywords) and `ledgerDebitCents`, which is what was actually charged. Mentions are settled the day after, so a period that ends today shows less than list price by today's mentions. Rows are paged with `limit` (up to 500) and `offset`, and come with a `total`. Keys and tokens can read the breakdown 30 times a minute.
### This month, per keyword
Each keyword also carries this month's numbers in `stats.cost`, so `GET /v1/keywords` shows what every keyword has cost so far this month.
```ts tab="TypeScript"
const { data: breakdown } = await rumoro.getUsageBreakdown({ query: { by: 'keyword', range: '30d', limit: 100 }, throwOnError: true });
for (const row of breakdown.data) if (row.totalCents > 0) console.log(row.label, row.totalCents);
```
```python tab="Python"
breakdown = rumoro.usage.breakdown(by="keyword", range_="30d", limit=100)
for row in breakdown.data:
if row.total_cents > 0:
print(row.label, row.total_cents)
```
```bash tab="curl"
curl -s "https://api.rumoro.dev/v1/usage/breakdown?by=keyword&range=30d&limit=100" \
-H "Authorization: Bearer $RUMORO_API_KEY" | jq '.data[] | select(.totalCents > 0) | {keyword: .label, cents: .totalCents}'
```
## The welcome credit
Every new account starts with $5.80, enough for one keyword for a month plus 100 mentions. Each person gets it once, without a card. It shows in the wallet as `signupCredit`.
Two guards protect the credit until your first top-up or a credit from our team. After that neither applies, although noisy keywords are still marked in `stats.noise.noisy`.
### 200 mentions per keyword
Each keyword can match up to 200 mentions a month, shown as `cap.welcome: true`. When it reaches 200 it reads `pausedForCap: true` and waits for the next month, without the daily keyword charge. If you set your own cap below 200, yours applies. A cap of 200 or more is kept in `cap.own` and takes over after your first top-up. While `cap.welcome` is true, sending `cap: { "mentions": 200 }` has no effect.
### The noise brake
A keyword with 20 or more scored matches in 14 days, of which less than 30% are relevant, is paused (`muted: true`, `pausedForNoise: true`, `status=noisy`). Owners get one email. If you change its terms, platforms or context, it starts again and is judged only on new matches, as long as the balance covers another day. You can also unmute it unchanged. A top-up doesn't restart it. [Keyword health](/guides/keyword-health) shows where the noise comes from.
## Adding funds
**Add funds** in the dashboard opens a Polar checkout. The default is $20, and you can add anything from $20 to $5,000. Polar is the seller of record and adds tax. Once the order is paid, the amount before tax is added to your balance and paused tracking starts again right away. Refunds are taken off the balance.
The billing page links to Polar's customer portal for cards, receipts and billing details.
## Auto recharge
An owner can set a threshold and an amount under **Auto recharge** on the billing page. When the effective balance falls below the threshold, or below one day of running keywords, Rumoro charges the saved card and adds the amount like a top-up. This also restarts a paused workspace.
- It charges at most once per workspace per UTC day.
- The amount can be $20 to $5,000, and the threshold anything from zero.
- If a charge fails, owners get an email and Rumoro tries again the next day.
The wallet shows the settings as `autoRecharge`. Only an owner can change them, on the billing page. API keys can't.
## When the balance runs out
Tracking pauses when the effective balance can't pay for another day of running keywords (`nextDayCents`). That way you are never charged for a day you don't get. Nothing is deleted. Paused keywords are counted in `autoMutedKeywords`.
Tracking starts again as soon as a top-up covers one day of all keywords, running and paused (`resumeCostCents`).
Owners get an email when the effective balance drops to 20% of the last credit (once per credit), and every time tracking pauses.
## Keyword limit
A workspace can track up to 500 keywords. If you need more, [write to us](mailto:markus@rumoro.dev).
Creating or unmuting a keyword fails with `402 insufficient_balance` when the effective balance can't pay for another keyword-day, and with `402 keyword_limit_reached` at 500. The check happens in the same database write, so parallel requests can't get around it. See [Errors](/errors).
## Capping a keyword's mentions
To limit how much a keyword can spend on mentions in a month, for example when you resell Rumoro, set `cap: { "mentions": 500 }` on [`POST /v1/keywords`](/api/keywords/create-keyword) or [`PATCH /v1/keywords/{id}`](/api/keywords/update-keyword). `null` removes the cap.
- The cap counts `stats.thisMonth`, which is every match in the current UTC month, look-back included.
- At the cap the keyword stops matching, reads `pausedForCap: true` and shows up under `status=capped`. Owners get one email, and webhooks can subscribe to [`keyword.capped`](/webhooks/account-events).
- It starts again on the first of the next month, or when a `PATCH` raises or removes the cap.
- The daily keyword charge continues. Set `muted: true` to stop that as well.
## The wallet through the API
| Call | What it does |
| --- | --- |
| [`GET /v1/billing/wallet`](/api/billing/get-wallet) | The balance, top-up limits and auto recharge settings |
| [`GET /v1/billing/ledger`](/api/billing/list-ledger) | Every change to the balance, newest first, with cursor paging |
| [`POST /v1/billing/top-ups`](/api/billing/create-top-up) | Returns a checkout link for `amountCents`. An optional `successUrl` must be on app.rumoro.dev. Charges nothing by itself, 5 a minute. |
| [`GET /v1/billing/invoices`](/api/billing/list-invoices), [`GET /v1/billing/invoices/{id}/url`](/api/billing/get-invoice-url) | Receipts, and a short-lived link to each PDF |
A `read` key can read all of this. Creating a checkout needs `write`. Cards and the customer portal are only in the dashboard.
### Checking your balance
```ts tab="TypeScript"
const { data: wallet } = await rumoro.getWallet();
```
```python tab="Python"
wallet = rumoro.billing.wallet()
```
```bash tab="curl"
curl https://api.rumoro.dev/v1/billing/wallet -H "Authorization: Bearer $RUMORO_API_KEY"
```
```json
{
"balanceCents": 2615,
"pendingCents": 72,
"effectiveBalanceCents": 2543,
"burnPerDayCents": 118,
"daysLeft": 21,
"stopped": false,
"lowBalance": false,
"activeKeywords": 4,
"autoMutedKeywords": 0,
"nextDayCents": 67,
"resumeCostCents": 67,
"signupCredit": { "amountCents": 580, "grantedAt": "2026-09-21T14:05:47.000Z" },
"lastTopUpAt": "2026-10-01T08:33:19.000Z",
"billingConfigured": true,
"minTopUpCents": 2000,
"maxTopUpCents": 500000,
"defaultTopUpCents": 2000,
"currency": "USD",
"autoRecharge": { "available": true, "enabled": false, "thresholdCents": 500, "amountCents": 2000, "lastRunAt": null, "lastError": null }
}
```
| Field | Meaning |
| --- | --- |
| `balanceCents` | Credits minus settled charges |
| `pendingCents` | Matches since the last settlement, priced but not charged yet |
| `effectiveBalanceCents` | `balanceCents` minus `pendingCents`, which the pause rule uses |
| `burnPerDayCents` | Average daily charge over the last 7 days, or since the workspace was created |
| `daysLeft` | Effective balance divided by the daily charge, rounded down. `null` when nothing is charged. |
| `stopped` | `true` while tracking is paused for lack of balance |
| `lowBalance` | `true` at or below 20% of the last credit |
| `activeKeywords` | Keywords running now |
| `autoMutedKeywords` | Keywords paused for lack of balance |
| `nextDayCents` | What one more day of the running keywords costs |
| `resumeCostCents` | What one day of all keywords costs. Tracking restarts once the balance covers it. |
| `signupCredit` | The welcome credit (`amountCents`, `grantedAt`), or `null` |
| `lastTopUpAt` | The latest paid top-up, or `null` |
| `billingConfigured` | `false` where top-ups aren't available |
| `minTopUpCents`, `maxTopUpCents`, `defaultTopUpCents` | The allowed top-up range (2000 to 500000) and the default (2000) |
| `currency` | Always `USD` |
| `autoRecharge` | Auto recharge settings and the last run |
### The ledger
```ts tab="TypeScript"
const { data: ledger } = await rumoro.listLedger({ query: { limit: 50 } });
```
```python tab="Python"
ledger = rumoro.billing.ledger(limit=50)
```
```bash tab="curl"
curl "https://api.rumoro.dev/v1/billing/ledger?limit=50" -H "Authorization: Bearer $RUMORO_API_KEY"
```
```json
{
"data": [
{ "id": "led_...", "kind": "debit_mentions", "amountCents": -72, "day": "2026-10-03", "units": 2410, "note": null, "polarOrderId": null, "createdAt": "2026-10-04T00:01:38.000Z" },
{ "id": "led_...", "kind": "topup", "amountCents": 2500, "day": null, "units": null, "note": null, "polarOrderId": "7f3e91c0-...", "createdAt": "2026-10-01T08:33:19.000Z" }
],
"nextCursor": null
}
```
Credits are positive and charges negative. Use `cursor` for the next page. On a charge, `day` is the last UTC day it settles, and `units` is the running total of keyword-days or mentions settled so far.
| Kind | Meaning |
| --- | --- |
| `signup_credit` | The welcome credit |
| `topup` | A paid Polar order, with its `polarOrderId` |
| `refund` | A Polar refund, taken off the balance |
| `debit_keyword_days` | The daily keyword charge |
| `debit_mentions` | The daily mention charge |
| `adjustment` | A manual correction by the Rumoro team, with a `note` |
Each charge bills the total owed so far minus what was already charged, so rounding errors never build up. 30 keyword-days are exactly 500 cents and 100 mentions exactly 80.
Where top-ups aren't available, the top-up endpoints return `503 billing_not_configured` and `billingConfigured` is `false`. Everything else keeps working.
# Changelog
What changed in Rumoro, newest first. Also as an RSS feed.
The newest changes come first. You can also follow them by [RSS](/changelog/rss.xml).
## October 5, 2026: Code in TypeScript, Python and curl
Improved. The docs show each API call three ways, and agents can ask for any page as Markdown.
- **Code tabs.** Every REST example in the guides shows the call with the [TypeScript SDK](/sdks/typescript), the [Python SDK](/sdks/python) and curl. The language you pick stays picked on every page. Slack and Telegram setup stays curl only.
- **Markdown on request.** Ask for a page with `Accept: text/markdown` and you get its Markdown, as with `.mdx` at the end of its address. Every page links its Markdown copy, and the site has a sitemap.
- **Was this page helpful?** Each page ends with a vote and an optional comment. We keep the page, the answer and the comment. No account or IP address is stored with them.
- **SDKs and CLI 0.1.2.** `@rumoro-dev/sdk`, `rumoro` and `@rumoro-dev/cli` carry the new endpoint descriptions. Calls work as before.
## October 5, 2026: One text for a post that matches several keywords
Improved. A post that matches several of your keywords now carries the passage around each of them, in the order they appear. Every mention of that post shows the same text. Before, each mention kept only the passage around its own keyword. Mentions stored before October 5 keep their text.
## October 5, 2026: Passages across line breaks
Fixed. When a long post breaks a line inside your keyword, the mention now shows the passage around it. Links in a post no longer end with a stray `]`.
## October 4, 2026: News mentions are back
Fixed. News stopped arriving on October 3 at 15:26 UTC, because the news source kept answering "not found" for its newest files. Rumoro now reads them from another address of the same source, and caught up on the missed hours on October 4.
## October 4, 2026: Your workspace opens first
Improved. Signing in now takes you straight to the workspace you used last.
- **Straight to your mentions.** Signing in, `app.rumoro.dev` and the website's Log in link open the Mentions page of the workspace you used last, or its setup if that isn't finished. An account without a workspace is asked to create one.
- **Connecting a client.** The MCP sign-in page names the client, with its logo for the common ones, and preselects the workspace you have open. `rumoro auth:login` adds its key to that workspace and shows where the key goes before you authorize.
- **Invitations.** The invitation page says whether you joined, and offers to sign out and use another account when the invitation was for a different address.
- **Docs and webhooks.** The Documentation link in the sidebar opens docs.rumoro.dev, and the webhook page's verification sample checks the timestamped signature.
## October 4, 2026: Author privacy
Improved. People whose posts Rumoro stores can now have them removed, and the MCP tools no longer return email addresses.
- **Email addresses stay out of MCP.** `list_people`, `get_person` and the other people tools return `profile.email` as `null`. The REST API and the dashboard still show it where the person published it.
- **Ask to be removed.** Anyone can request removal of their accounts at [rumoro.dev/privacy/remove](https://rumoro.dev/privacy/remove). We email a receipt right away and finish within 30 days.
- **What a removal deletes.** The person's mentions leave every workspace, with the notes, tags and outreach attached to them. Their profile is emptied and Rumoro stops storing their new posts. Charges stay in your ledger and usage.
- **Updated privacy policy.** The [privacy policy](https://rumoro.dev/privacy) now covers author data, its legal basis and who controls the notes a workspace adds.
## October 4, 2026: API fixes
Fixed.
- Every `429 rate_limited` now includes `retryAfterSeconds`, including the limits on exports, the usage breakdown, checkouts and invitations.
- The [error list](/errors) in the OpenAPI document now matches the codes the API actually returns.
- Owners and admins signed in through OAuth can manage the team, as in the dashboard.
- A digest that fails to send is retried as it was, even if you triage one of its mentions in the meantime.
- A digest whose rule sets its own `event` name now reaches Slack and Telegram as a digest.
- A channel test now names its rule `Test message`.
- The API reference, SDKs (`@rumoro-dev/sdk` and `rumoro` 0.1.1) and CLI (0.1.1) have clearer descriptions for every endpoint and field.
## October 4, 2026: Documentation
New. docs.rumoro.dev covers all of Rumoro, with guides, a page for every platform and a reference for every endpoint.
- **API reference with a playground.** Send real requests with your own key.
- **For agents.** Every page is available as Markdown by adding `.mdx` to its address. The whole site is in [llms.txt](/llms.txt) and [llms-full.txt](/llms-full.txt), and a read-only [documentation MCP server](/mcp#documentation-mcp-server) runs at `https://docs.rumoro.dev/api/mcp`.
- **Moving from Octolens.** A [guide](/migrate/octolens) with a script that copies your keywords, feeds and filters.
## October 4, 2026: search_mentions returns 10 mentions and hasMore
Fixed. The MCP tool `search_mentions` now returns 10 mentions by default as `{ "data", "hasMore" }`, as its description says. Before, it returned 25 with a cursor the tool couldn't take back.
## October 3, 2026: SDKs for TypeScript and Python, and a CLI
New. Three official clients, generated from the OpenAPI document.
- [`@rumoro-dev/sdk`](/sdks/typescript) on npm has one typed function per endpoint.
- [`rumoro`](/sdks/python) on PyPI has `Rumoro` and `AsyncRumoro`, and raises `RumoroError`.
- [`@rumoro-dev/cli`](/cli) on npm turns every endpoint into a `rumoro` command. It also signs in through the browser with `rumoro auth:login`, streams new mentions with `mentions:watch`, and prints MCP client settings with `mcp:config`.
## October 3, 2026: OAuth sign-in for MCP clients
New. MCP clients that support OAuth only need the server address. You sign in, choose a workspace and allow read or write access. API keys keep working. See [MCP](/mcp).
## October 3, 2026: Rumoro opens
New. Sign up and get $5.80 of credit, without a card.
- **Platforms.** X, Bluesky, Hacker News, Reddit, GitHub, Stack Overflow, DEV, YouTube, LinkedIn, TikTok, Instagram and news, plus reviews on the App Store, Google Play, Trustpilot and Google ([Platforms](/platforms)).
- **Mentions.** Relevance, sentiment and intent scored against your company profile, plus triage, views, people, segments, analytics, share of voice and CSV and JSON exports.
- **Alerts.** Instant, hourly, daily and weekly, to Slack, Telegram, email and signed webhooks, plus [Needs attention](/guides/attention).
- **Keywords.** Matching rules, [groups](/guides/groups), monthly caps and [keyword health](/guides/keyword-health).
- **For developers and agents.** The REST API, the MCP server and an [Octolens-compatible API](/migrate/octolens).
- **Billing.** Prepaid, $5 per keyword a month and $0.008 per matched mention ([Billing](/billing)).
# Platforms
Which platforms Rumoro searches, how often, and what a new keyword finds on its first day.
Rumoro searches 12 platforms for your keyword. It can also follow reviews on the App Store, Google Play, Trustpilot and Google Maps. All mentions land in one feed with the same fields, filters, alerts and price. To see only some platforms, filter with `platforms=reddit,x`.
## At a glance
| Platform | `platform` | Finds | Checked | New keyword |
| --- | --- | --- | --- | --- |
| [X](/platforms/x) | `x` | Posts and replies | Every hour | Last 30 days |
| [Bluesky](/platforms/bluesky) | `bluesky` | Every post with the term | Within seconds | Last 30 days |
| [LinkedIn](/platforms/linkedin) | `linkedin` | Public posts | Every 3 hours | Last 30 days |
| [Reddit](/platforms/reddit) | `reddit` | Posts | Every 30 minutes | Last 30 days |
| [TikTok](/platforms/tiktok) | `tiktok` | Captions and hashtags of new videos | Every 3 hours | Last 30 days |
| [Instagram](/platforms/instagram) | `instagram` | Captions and hashtags of public posts and reels | Every 6 to 12 hours | Last 10 days |
| [Hacker News](/platforms/hackernews) | `hackernews` | Stories and comments | Every 5 minutes | Last 30 days |
| [GitHub](/platforms/github) | `github` | Issues and pull requests | Every 15 minutes | Last 30 days |
| [Stack Overflow](/platforms/stackoverflow) | `stackoverflow` | Questions and answers | Every hour | Last 30 days |
| [DEV](/platforms/devto) | `devto` | New articles | Every hour | From now on |
| [YouTube](/platforms/youtube) | `youtube` | Video titles and descriptions | Every 12 hours | Last 30 days |
| [News](/platforms/news) | `news` | English-language news articles | Every 15 minutes | From now on |
| [App Store](/platforms/appstore) | `appstore` | Reviews of the apps a keyword names | Daily | 100 reviews, free |
| [Google Play](/platforms/googleplay) | `googleplay` | Reviews of the apps a keyword names | Daily | 100 reviews, free |
| [Trustpilot](/platforms/trustpilot) | `trustpilot` | Reviews on the Trustpilot pages a keyword names | Daily | 100 reviews, free |
| [Google reviews](/platforms/googlemaps) | `googlemaps` | Reviews of the places a keyword names | Daily | 100 reviews, free |
A new keyword starts with the 10 newest matching posts from the last 30 days on each platform (10 days on Instagram), so its feed isn't empty on day one. Review pages bring their 100 newest reviews for free, see [Reviews](/platforms/reviews). After that, Rumoro collects new posts as they appear.
On X, LinkedIn, Reddit, TikTok, Instagram and GitHub, a keyword that keeps finding nothing is checked less often, down to once or twice a day. Its next match brings it back to normal. How fresh a mention is depends mostly on the platform.
`polling` on a keyword shows when each platform was last checked and how many checks in a row found nothing. Bluesky, DEV and News are not listed there.
## Choosing platforms
By default a keyword searches every platform. Send `platforms` with a list to choose, or `[]` for a keyword that only [collects reviews](/platforms/reviews). `null` means every platform Rumoro has when the keyword is created. Platforms you leave out cost nothing.
```ts tab="TypeScript"
await rumoro.updateKeyword({ path: { id: 'kw_...' }, body: { platforms: ['github', 'stackoverflow', 'bluesky'] } });
```
```python tab="Python"
rumoro.keywords.update("kw_...", platforms=["github", "stackoverflow", "bluesky"])
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/keywords/kw_... \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "platforms": ["github", "stackoverflow", "bluesky"] }'
```
## Price
A keyword costs $5 a month and each matched mention $0.008, on every platform. See [Billing](/billing).
# X
Posts and replies on X that mention your keyword, with engagement and author details.
Rumoro searches X every hour for new posts and replies with your keyword. Reposts are skipped.
| `platform` | `x` |
| --- | --- |
| Finds | Posts and replies, newest first |
| Checked | Every hour, once a day if nothing is found |
| New keyword | The 10 newest posts from the last 30 days |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | The full post. Links are expanded, except links to photos, videos and quoted posts on X. |
| `post.engagement` | Likes, reposts, replies, quotes, views and bookmarks when Rumoro found the post |
| `author` | Name, handle, profile link, avatar and follower count. Bio, location, website and following count are on the [person](/api/people/get-person). |
| `post.replyTo` | For a reply, the parent post's author, link and text |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use `x.com/name`, `twitter.com/name`, a post link or `@name`. See [Mute authors](/alerts#mute-authors).
## Limits
- Each check keeps the 100 newest posts per keyword.
- Engagement is saved once and not updated.
# Bluesky
Public Bluesky posts with your keyword, picked up within seconds.
Rumoro reads Bluesky's public stream live and keeps every post with your keyword.
| `platform` | `bluesky` |
| --- | --- |
| Finds | Every post with the term |
| Checked | Within seconds |
| New keyword | The 10 newest posts from the last 30 days |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | The post's text. Media-only posts are skipped. |
| `post.engagement` | Likes, reposts, replies, quotes and bookmarks when Rumoro found the post |
| `post.imageUrl` | Image or link card, if any |
| `author` | Name (or handle), profile link, avatar and follower count. Following and post counts are on the [person](/api/people/get-person). |
| `post.replyTo` | For a reply, the parent post's author, link and text |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use the profile link (`bsky.app/profile/name.bsky.social`), `name.bsky.social` or the DID (`did:plc:…`). See [Mute authors](/alerts#mute-authors).
## Limits
- Image alt text and link cards aren't matched.
- A new keyword may get fewer than 10 older posts.
- A new keyword starts matching within about two minutes.
# LinkedIn
Find your keyword in public LinkedIn posts by people and companies.
Rumoro searches LinkedIn for public posts with your keyword, newest first. It reads only what anyone can see without logging in, so no comments or reactions.
| `platform` | `linkedin` |
| --- | --- |
| Finds | Public posts by people and company pages |
| Checked | Every 3 hours, once a day if nothing is found |
| New keyword | The 10 newest posts from the last 30 days |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | The post's text |
| `author` | Name, avatar, and a link to the profile or company page. A person's headline, and a company page's followers and website, are on the [person](/api/people/get-person). |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use `linkedin.com/in/name`, `linkedin.com/company/name` or the handle. Company `/posts` links and school or showcase pages work too. See [Mute authors](/alerts#mute-authors).
## Limits
- There are no engagement counts.
# Reddit
Reddit posts with your keyword, from every subreddit.
Rumoro searches all of Reddit for posts with your keyword as an exact phrase, newest first.
| `platform` | `reddit` |
| --- | --- |
| Finds | Posts, by title and text |
| Checked | Every 30 minutes, twice a day if nothing is found |
| New keyword | The 10 newest posts from the last 30 days |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | Title, text and subreddit |
| `author` | Username and profile link. Empty for a deleted account. |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use `reddit.com/user/name`, `u/name` or the username. Subreddit and post links don't name a person, so they are refused. See [Mute authors](/alerts#mute-authors).
## Limits
- Comments aren't searched.
- A link post has no text. It is matched on its title and subreddit.
- Each check rereads the last day to catch posts Reddit lists late.
- The workspace filter `subreddits` keeps or drops subreddits. See [Filters](/api/filters/update-filters).
# TikTok
New TikTok videos with your keyword in the caption, with views and likes.
Rumoro searches TikTok for new videos with your keyword in the caption. TikTok's search returns loose matches, so Rumoro checks each result against the keyword. You don't need a TikTok account.
| `platform` | `tiktok` |
| --- | --- |
| Finds | Captions and hashtags of new videos |
| Checked | Every 3 hours, twice a day if nothing is found |
| New keyword | The 10 newest videos from the last 30 days |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | The caption, with its hashtags |
| `post.engagement` | Views and likes, comments as `replies`, shares as `reposts` and saves as `bookmarks` |
| `author` | Name, profile link, avatar and follower count |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use `tiktok.com/@name`, a video link or `@name`. Short `vm.tiktok.com` links are refused. Open one and use the full link instead. See [Mute authors](/alerts#mute-authors).
## Limits
- Only the caption is searched, not what is said in the video.
- Each check looks back over the last day, so videos TikTok lists late are still found.
- Keywords created before TikTok was added in September 2026 don't search it. Add `tiktok` to their platforms to turn it on.
# Instagram
Public Instagram posts and reels with your keyword in the caption or hashtag. No Instagram login needed.
Instagram's search shows popular posts, not new ones. So Rumoro uses two routes. It finds captions through public web search, up to twice a day. For a one-word keyword it also reads every new post under that hashtag. You don't need an Instagram login.
| `platform` | `instagram` |
| --- | --- |
| Finds | Captions and hashtags of public posts and reels |
| Checked | Hashtags every 6 hours, captions up to twice a day, about once a day if nothing is found |
| New keyword | The 10 newest posts from the last 10 days |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | The caption, plus the search snippet when that adds something |
| `post.engagement` | Likes, and comments as `replies` |
| `author` | Name, profile link and avatar when Instagram provides them. Posts found by hashtag can lack a display name. |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use `instagram.com/name` or the handle. Post and reel links don't name the author, so they are refused. See [Mute authors](/alerts#mute-authors).
## Limits
- Only one-word keywords use the hashtag route. A phrase is found through captions only.
- Caption matches can arrive late, because web search lists new posts with a delay.
- There are no view counts.
- Keywords created before Instagram was added in September 2026 don't search it. Add `instagram` to their platforms to turn it on.
# Hacker News
Hacker News stories and comments with your keyword, searched every 5 minutes.
Rumoro searches Hacker News stories and comments for each keyword every 5 minutes and keeps the ones with the term.
| `platform` | `hackernews` |
| --- | --- |
| Finds | Stories and comments |
| Checked | Every 5 minutes |
| New keyword | The 10 newest posts from the last 30 days |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | A story's title and text, or a comment's text |
| `author` | Username and profile link |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use `news.ycombinator.com/user?id=name` or the username. Story links are refused. See [Mute authors](/alerts#mute-authors).
## Limits
- Changing a keyword's term starts a new look-back.
# GitHub
Public GitHub issues and pull requests with your keyword in the title or body.
Rumoro looks for your keyword in the title and body of public GitHub issues and pull requests.
| `platform` | `github` |
| --- | --- |
| Finds | Issues and pull requests |
| Checked | Every 15 minutes, once a day if nothing is found |
| New keyword | The 10 newest posts from the last 30 days |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | Title and body |
| `author` | Login, profile link and avatar. A later profile lookup adds the name and followers, and puts the bio, company, location, website, public email and linked accounts on the [person](/api/people/get-person). |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use `github.com/name`, `github.com/orgs/name`, the login, or a repository link to mute its owner. See [Mute authors](/alerts#mute-authors).
## Limits
- Issue and pull request comments are not searched.
- Search can list a new issue late, so each check rereads the last hour.
- The workspace filter `excludedRepos` leaves out repositories.
# Stack Overflow
Stack Overflow questions and answers with your keyword, checked every hour.
Every hour, Rumoro looks for your keyword in Stack Overflow questions and answers.
| `platform` | `stackoverflow` |
| --- | --- |
| Finds | Questions and answers |
| Checked | Every hour |
| New keyword | The 10 newest posts from the last 30 days |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | The question's title and a short excerpt of the question or answer |
| `author` | Display name, profile link and avatar |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use the profile link, such as `stackoverflow.com/users/123/name`. See [Mute authors](/alerts#mute-authors).
## Limits
- Rumoro gets an excerpt, not the full post. An answer doesn't match on its question's title alone.
- Stack Exchange limits requests per day. On a very busy day checks can stop until midnight UTC, then catch up.
# DEV
New articles on DEV (dev.to) with your keyword in the title, description or tags.
Every hour, Rumoro reads all new articles on DEV and keeps the ones with a keyword.
| `platform` | `devto` |
| --- | --- |
| Finds | New articles |
| Checked | Every hour |
| New keyword | No look-back. A keyword starts with articles published after it was created. |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | Title, description and tags |
| `post.imageUrl` | Cover image |
| `author` | Name, profile link and avatar |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Mute with the handle, `dev.to/name` or an article link. See [Mute authors](/alerts#mute-authors).
## Limits
- The article body isn't read. A keyword that appears only in the body is not found.
- Comments aren't read.
# YouTube
New YouTube videos with your keyword in the title or description.
Rumoro searches YouTube for new videos with your keyword in the title or description.
| `platform` | `youtube` |
| --- | --- |
| Finds | Video titles and descriptions |
| Checked | Every 12 hours |
| New keyword | The 10 newest videos from the last 30 days |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | Title and full description |
| `post.imageUrl` | Thumbnail |
| `author` | Channel name and link |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use the channel link, such as `youtube.com/channel/UC…`, or the channel name. An `@handle` link is saved but never matches, because mentions carry the channel link and name. See [Mute authors](/alerts#mute-authors).
## Limits
- Only the title and description are searched, not what is said in the video.
- YouTube can take days to show a new video in search, so some videos arrive late.
- Comments aren't read.
# News
English-language news articles with your keyword in the headline or among the people and organisations they name.
Every 15 minutes, Rumoro reads the GDELT feed of English-language news and keeps the articles that name a keyword.
| `platform` | `news` |
| --- | --- |
| Finds | English-language news articles |
| Checked | Every 15 minutes |
| New keyword | No look-back. A keyword starts with articles published after it was created. |
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | Headline, site, and the people and organisations named |
| `post.imageUrl` | Sharing image |
| `author` | The publishing site's domain and link |
Score, sentiment, intent and triage work the same on every platform. See the [mention fields](/api/mentions/get-mention).
## Mute an author
Use the site's address, such as `techcrunch.com`, to mute that publication. See [Mute authors](/alerts#mute-authors).
## Limits
- The article body isn't read. A keyword that appears only there is not found.
- Only English-language articles are covered.
- After an interruption, Rumoro catches up on up to 24 hours of news.
News data comes from [The GDELT Project](https://www.gdeltproject.org/).
# Reviews
How a keyword collects reviews from the App Store, Google Play, Trustpilot and Google.
A keyword can follow the reviews of your app, company or place. Add the review page to the keyword's `reviewSources` and each new review there becomes a mention, with its star rating. The review doesn't have to mention your name, and most don't ("the app crashes on login"). That's why review sites are not searched by the keyword's term like other platforms.
| Platform | `platform` | What to paste | Countries |
| --- | --- | --- | --- |
| [App Store](/platforms/appstore) | `appstore` | The app's App Store link | Yes, `countries` |
| [Google Play](/platforms/googleplay) | `googleplay` | The app's Google Play link | Yes, `countries` and `language` |
| [Trustpilot](/platforms/trustpilot) | `trustpilot` | The company page, `trustpilot.com/review/` | No, one page |
| [Google reviews](/platforms/googlemaps) | `googlemaps` | The place's Google Maps link, a `maps.app.goo.gl` share link, or its Place ID | No, one place |
A keyword can search its term, follow review pages, or both.
| `platforms` | `reviewSources` | What the keyword collects |
| --- | --- | --- |
| `null` or a list | none | Posts with the term |
| `null` or a list | one or more pages | Posts with the term, plus every review on the pages |
| `[]` | one or more pages | Only the reviews. The term is not searched. |
A keyword with `platforms: []` and no review pages would collect nothing, so it is refused with `400 validation_error`.
Reviews go into the same feed as other mentions. You can triage, filter, export and analyse them, and alert rules see them too.
## Connect a review page
Add `reviewSources` on create or with `PATCH /v1/keywords/{id}`. Entries are links, or a `platform` and `id` pair.
```ts tab="TypeScript"
await rumoro.createKeyword({
body: {
term: 'Slack',
kind: 'brand',
reviewSources: [
{ url: 'https://apps.apple.com/us/app/slack/id618783545', countries: ['us', 'gb'] },
{ url: 'https://play.google.com/store/apps/details?id=com.Slack', language: 'en' },
{ url: 'https://www.trustpilot.com/review/slack.com' },
],
},
});
```
```python tab="Python"
rumoro.keywords.create(
term="Slack",
kind="brand",
reviewSources=[
{"url": "https://apps.apple.com/us/app/slack/id618783545", "countries": ["us", "gb"]},
{"url": "https://play.google.com/store/apps/details?id=com.Slack", "language": "en"},
{"url": "https://www.trustpilot.com/review/slack.com"},
],
)
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/keywords \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"term": "Slack",
"kind": "brand",
"reviewSources": [
{ "url": "https://apps.apple.com/us/app/slack/id618783545", "countries": ["us", "gb"] },
{ "url": "https://play.google.com/store/apps/details?id=com.Slack", "language": "en" },
{ "url": "https://www.trustpilot.com/review/slack.com" }
]
}'
```
| Field | Meaning |
| --- | --- |
| `url` | Link to an App Store or Google Play app, a Trustpilot page or a Google Maps place |
| `platform`, `id` | Use these instead of `url`. `appstore` takes the app's number, `googleplay` the package name (`com.Slack`), `trustpilot` the domain (`slack.com`), `googlemaps` a Place ID (`ChIJ...`) or CID. |
| `countries` | App Store and Google Play only. Two-letter codes of the storefronts to read, up to 20. Defaults to the one in the link, or `us`. Reviews are kept per storefront, so an app mostly reviewed in Germany needs `de`. |
| `language` | Google Play only. The review language to read, such as `en`, `es` or `pt-BR`. Defaults to the link's `hl`, or `en`. |
A keyword can have up to 10 review pages. `PATCH` replaces the whole list. Sending `[]` disconnects all pages and keeps the reviews already collected. The keyword's `reviewSources` shows each page's `platform`, `id`, `url`, `countries`, `language` and `connectedAt`.
You can follow a competitor's app the same way. Create a `competitor` keyword on their app and add an alert rule with `ratings: [1, 2]`, and you get a daily list of their unhappy users. Send `"platforms": []` to collect only the reviews, without posts that mention them.
```ts tab="TypeScript"
await rumoro.createKeyword({
body: {
term: 'Discord',
kind: 'competitor',
platforms: [],
reviewSources: [{ url: 'https://apps.apple.com/us/app/discord-talk-play-hang-out/id985746746' }],
},
});
```
```python tab="Python"
rumoro.keywords.create(
term="Discord",
kind="competitor",
platforms=[],
reviewSources=[{"url": "https://apps.apple.com/us/app/discord-talk-play-hang-out/id985746746"}],
)
```
```bash tab="curl"
curl -X POST https://api.rumoro.dev/v1/keywords \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "term": "Discord", "kind": "competitor", "platforms": [],
"reviewSources": [{ "url": "https://apps.apple.com/us/app/discord-talk-play-hang-out/id985746746" }] }'
```
A keyword costs $5 a month either way, and each review is billed like any other mention.
## What a review looks like
A review is a normal mention with an extra `review` object.
```json
"review": {
"rating": 2,
"ratingMax": 5,
"title": "Notifications stopped working",
"version": "25.09.10",
"country": "gb",
"verified": null,
"response": null,
"responseAt": null,
"app": { "platform": "appstore", "id": "618783545", "url": "https://apps.apple.com/gb/app/id618783545" }
}
```
`review` is `null` for posts that aren't reviews.
- `response` is the reply from the owner or developer. The App Store feed never includes one.
- `title` exists on the App Store and Trustpilot, `version` on both app stores, `verified` on Trustpilot.
- On the app stores `country` is the storefront. On Trustpilot it is where the reviewer lives.
- `app` is the review page, on every platform.
Reviews are scored differently from posts.
| | How it works |
| --- | --- |
| Relevance | Always relevant, because you chose the page. 95 for a `brand` keyword, 75 for `competitor`, 60 for `topic`. |
| Sentiment | From the stars, not the text. 4 and 5 stars are positive, 3 neutral, 1 and 2 negative. |
| Intents | Read from the text, so a review can be tagged `bug_report`, `churn_intent`, `pricing`, `praise` and so on. It also gets a language. |
| Date | `post.publishedAt` is the writing time, not the time Rumoro found the review. |
## Filter reviews
`ratings` keeps only reviews with the given star ratings, and `notRatings` removes them. Both work on `GET /v1/mentions`, exports, views and alert rules. Posts that aren't reviews never match `ratings`.
```ts tab="TypeScript"
const { data: page } = await rumoro.searchMentions({ query: { platforms: ['appstore', 'googleplay'], ratings: [1, 2] } });
```
```python tab="Python"
page = rumoro.mentions.search(platforms=["appstore", "googleplay"], ratings=[1, 2])
```
```bash tab="curl"
curl "https://api.rumoro.dev/v1/mentions?platforms=appstore,googleplay&ratings=1,2" \
-H "Authorization: Bearer $RUMORO_API_KEY"
```
The CSV export adds two columns, `rating` and `app_id` (the page's id).
## Review report
`GET /v1/analytics/reviews` summarises your reviews over a period. It takes the same options as the other analytics reports (`range`, `from` and `to`, `timezone`, `keywordIds`, `platforms`, `compare`), plus `bucket` set to `day` or `week`.
```ts tab="TypeScript"
const { data: report } = await rumoro.getReviewsReport({ query: { range: '30d', compare: true } });
```
```python tab="Python"
report = rumoro.analytics.reviews(range_="30d", compare=True)
```
```bash tab="curl"
curl "https://api.rumoro.dev/v1/analytics/reviews?range=30d&compare=true" \
-H "Authorization: Bearer $RUMORO_API_KEY"
```
| Part | Holds |
| --- | --- |
| `totals` | Review count, average rating, split by stars, replied reviews and open 1 and 2 star ones. Two keywords matching one review count it once. |
| `tags` | Tags on 1 and 2 star reviews, most common first. It shows what unhappy reviewers complain about. |
| `pages` | The same numbers per review page, with the average rating per day or week |
| `previous` | With `compare=true`, the numbers for the equal period before |
The same report is `rumoro analytics:reviews` in the CLI, `get_reviews_report` in MCP, and the **Reviews** page in the dashboard.
## How reviews are collected
- **Once a day** for each page (and each storefront on the app stores). Workspaces that follow the same page share the read. Each read also covers the two days before, because Apple lists some reviews late.
- **The first 30 days are free.** Connecting a page, or adding a storefront or language, brings in its 100 newest reviews from the last 30 days. They are not billed and don't trigger instant alerts, but they show in the feed and digests. This happens once per page per workspace. Disconnecting and reconnecting the page, or deleting the keyword and creating it again, brings no new free reviews. Two keywords on the same page share one free batch.
- **After that, each new review is billed** like any mention. A review seen in two storefronts counts once, and an edited review isn't counted again.
- **Each storefront is separate.** A keyword gets reviews from the storefronts it lists. A country added later starts that day, with its own free 30 days.
- **A muted keyword** stops collecting. When you unmute it, the two-day overlap can bring up to about three days of reviews written in the meantime, billed as new.
- **The monthly mention cap** applies to new reviews too, which limits the bill for a very busy app. The free reviews don't count toward it. Each page is read up to its 1,000 newest reviews a day.
# App Store
New App Store reviews of your apps, per country, with their star rating.
The App Store is a [review site](/platforms/reviews), so Rumoro doesn't search it for your term. You add apps to a keyword's `reviewSources`, and every new review of those apps becomes a mention, even if it doesn't name the app.
| `platform` | `appstore` |
| --- | --- |
| Finds | Every new review of the apps you add, per storefront (country) |
| Checked | Once a day for each app and storefront |
| New page | The 100 newest reviews per storefront, free |
| Source | Apple's public review feed |
## Add an app
Paste the app's App Store link and list the storefronts to read. Without `countries`, Rumoro uses the one in the link, or `us`.
```ts tab="TypeScript"
await rumoro.updateKeyword({
path: { id: 'kw_...' },
body: { reviewSources: [{ url: 'https://apps.apple.com/us/app/slack/id618783545', countries: ['us', 'gb', 'de'] }] },
});
```
```python tab="Python"
rumoro.keywords.update(
"kw_...", reviewSources=[{"url": "https://apps.apple.com/us/app/slack/id618783545", "countries": ["us", "gb", "de"]}]
)
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/keywords/kw_... \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reviewSources": [
{ "url": "https://apps.apple.com/us/app/slack/id618783545", "countries": ["us", "gb", "de"] }
] }'
```
You can also send the number after `id` in the link, as `{ "platform": "appstore", "id": "618783545" }`. Each storefront has its own reviews. For an app reviewed mostly in Germany, add `de`. A review seen in two storefronts counts once.
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | Review title, a blank line, then the review |
| `review.rating` | 1 to 5 |
| `review.title`, `review.version`, `review.country` | Title, the reviewer's app version, and country |
| `author` | Name and a link to the reviewer's App Store profile |
| `review.response` | Always `null`. Apple's feed leaves out developer replies. |
Sentiment comes from the stars and relevance from the keyword's kind. Intents are still read from the text.
## Limits
- Apple's feed shows the 500 newest reviews per storefront. A daily read only hits that limit for the very biggest apps.
- Apple lists a review a few hours after it is written, with the time it was written. So a review can show up a day after its date.
- A storefront where the app isn't sold stays empty.
# Google Play
New Google Play reviews of your apps, per country and language, with their star rating.
Google Play is a [review site](/platforms/reviews), so Rumoro doesn't search it for your term. You add apps to a keyword's `reviewSources`, and every new review of those apps becomes a mention, even if it doesn't name the app.
| `platform` | `googleplay` |
| --- | --- |
| Finds | Every new review of the apps you add, per storefront and language |
| Checked | Once a day for each app, storefront and language |
| New page | The 100 newest reviews per storefront, free |
## Add an app
Use the Google Play link of the app. You may add `countries` and `language`.
```ts tab="TypeScript"
await rumoro.updateKeyword({
path: { id: 'kw_...' },
body: { reviewSources: [{ url: 'https://play.google.com/store/apps/details?id=com.Slack', countries: ['us'], language: 'en' }] },
});
```
```python tab="Python"
rumoro.keywords.update(
"kw_...",
reviewSources=[{"url": "https://play.google.com/store/apps/details?id=com.Slack", "countries": ["us"], "language": "en"}],
)
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/keywords/kw_... \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reviewSources": [
{ "url": "https://play.google.com/store/apps/details?id=com.Slack", "countries": ["us"], "language": "en" }
] }'
```
You can also send the package name from the link's `id=`, as `{ "platform": "googleplay", "id": "com.Slack" }`.
### Choose a language
Google Play returns reviews in one language at a time. An app read in `en` never shows its Spanish reviews. Without `language`, Rumoro uses the link's `hl`, or `en`. To follow two languages, add the app to two keywords, one for each language.
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | The review. Google Play reviews have no title. |
| `review.rating` | 1 to 5 |
| `review.version`, `review.country` | App version and country |
| `review.response`, `review.responseAt` | The developer's reply and its time, if any |
| `post.engagement.likes` | Helpful votes |
| `author` | Name and avatar. Google Play has no reviewer profiles, so every review has its own author. |
Sentiment comes from the stars and relevance from the keyword's kind. Intents are still read from the text.
## Limits
- 52 storefronts are supported, each a two-letter code like `us`, `gb`, `de`, `br` or `in`. Other codes are refused, and the error names them.
- Each day reads at most an app's 1,000 newest reviews per storefront.
# Trustpilot
New Trustpilot reviews of your company, with their star rating.
Trustpilot is a [review site](/platforms/reviews), so Rumoro doesn't search it for your term. You add Trustpilot pages to a keyword's `reviewSources`, and every new review on those pages becomes a mention, even if it doesn't name the company.
| `platform` | `trustpilot` |
| --- | --- |
| Finds | Every new review on the pages you add |
| Checked | Once a day for each page |
| New page | The 100 newest reviews, free |
## Add a page
Paste the company's Trustpilot link. Links from any country's site work, including `uk.trustpilot.com`.
```ts tab="TypeScript"
await rumoro.updateKeyword({ path: { id: 'kw_...' }, body: { reviewSources: [{ url: 'https://www.trustpilot.com/review/slack.com' }] } });
```
```python tab="Python"
rumoro.keywords.update("kw_...", reviewSources=[{"url": "https://www.trustpilot.com/review/slack.com"}])
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/keywords/kw_... \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reviewSources": [{ "url": "https://www.trustpilot.com/review/slack.com" }] }'
```
To use just the domain, send `{ "platform": "trustpilot", "id": "slack.com" }`. There are no countries, because Trustpilot has one page for everyone.
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | Review title, a blank line, then the review |
| `review.rating` | 1 to 5 |
| `review.title`, `review.verified`, `review.country` | Title, Trustpilot's verified badge, and the reviewer's country when given |
| `review.response`, `review.responseAt` | The company's reply and its time, if any |
| `post.engagement.likes` | "Useful" votes |
| `author` | Reviewer's name, Trustpilot profile and picture |
Sentiment comes from the stars and relevance from the keyword's kind. Intents are still read from the text.
## Limits
- Each day reads at most a page's 1,000 newest reviews.
- If Trustpilot removes a review after Rumoro found it, the mention stays in your feed.
# Google reviews
New Google reviews of your shop, restaurant, office or any other place on Google Maps, with their star rating.
Google reviews is a [review site](/platforms/reviews), so Rumoro doesn't search it for your term. You add places to a keyword's `reviewSources`, and every new Google review of those places becomes a mention. This works for anything with an address, such as a shop, restaurant, clinic or office.
| `platform` | `googlemaps` |
| --- | --- |
| Finds | Every new Google review of the places you add |
| Checked | Once a day for each place |
| New page | The 100 newest reviews, free |
## Add a place
Paste the place's Google Maps link. The long link from the address bar works, and so does the short `maps.app.goo.gl` link from Share. You can also send a Place ID (`ChIJ...`) or a CID.
```ts tab="TypeScript"
await rumoro.updateKeyword({ path: { id: 'kw_...' }, body: { reviewSources: [{ url: 'https://maps.app.goo.gl/...' }] } });
```
```python tab="Python"
rumoro.keywords.update("kw_...", reviewSources=[{"url": "https://maps.app.goo.gl/..."}])
```
```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/keywords/kw_... \
-H "Authorization: Bearer $RUMORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reviewSources": [{ "url": "https://maps.app.goo.gl/..." }] }'
```
The link has to open a single place, not a search or an area of the map. Places have no countries.
## What you get
| Field | Holds |
| --- | --- |
| `post.text` | The review. A rating without text reads "Rated 4 of 5 stars, with no written review." and is still shown and billed. |
| `review.rating` | 1 to 5 |
| `review.response`, `review.responseAt` | The owner's reply and its time, if any |
| `author` | Reviewer's name, Google Maps profile, picture and review count |
Sentiment comes from the stars and relevance from the keyword's kind. Intents are still read from the text.
## Limits
- Helpful votes aren't collected.
- Each day reads at most a place's 1,000 newest reviews.
# Introduction
Every Rumoro endpoint, generated from the OpenAPI document the API serves.
Send every request to this base URL.
```
https://api.rumoro.dev/v1
```
Authenticate with a Bearer API key from the dashboard, or an OAuth token from an [MCP sign-in](/mcp).
```ts tab="TypeScript"
import { createRumoro } from '@rumoro-dev/sdk';
const rumoro = createRumoro({ apiKey: 'ref_...' });
await rumoro.listKeywords();
```
```python tab="Python"
from rumoro import Rumoro
rumoro = Rumoro(api_key="ref_...")
rumoro.keywords.list()
```
```bash tab="curl"
curl https://api.rumoro.dev/v1/keywords \
-H "Authorization: Bearer ref_..."
```
These pages are built from the API's own [OpenAPI document](https://api.rumoro.dev/v1/openapi.json), which also validates requests. Each page's playground sends real requests to your workspace, so explore with a `read` key.
Start with [Keywords](/api/keywords/create-keyword) and [Mentions](/api/mentions/search-mentions), or read [Conventions](/conventions). Slack, Telegram and delivery-log retries have their own pages.
# Create a keyword
`POST /v1/keywords`
Starts monitoring a word or phrase. Matching, scoring and delivery start with the next poll. A workspace with balance can have up to 500 keywords, each costing $5 a month, taken from the balance day by day. `matching` narrows what counts as a match, with required and excluded terms, excluded authors and case, before anything is stored, so rejected posts are never billed. `context` is a sentence only this keyword's classifier reads. `cap` limits matched mentions per month. At the cap the keyword stops matching until the 1st of next month (UTC) or until you raise the cap, and its daily charge continues.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `term` | yes | The word or phrase to monitor. It matches as a whole phrase, ignoring case. |
| `kind` | | brand is for your own names, competitor for a rival's, and topic for your market. Share of voice and segments use it. |
| `platforms` | | The platforms to search for the term. Leave it out or send null for all platforms. An empty list searches nowhere, for a keyword that only collects reviews and so needs reviewSources. |
| `context` | | Up to 300 characters the classifier reads for this keyword only, in addition to the company profile or the group's description. Say what the term means for you and what to ignore, for example "Driftwood is our deploy tool, not beach wood." Null clears it. |
| `matching` | | Fields you leave out stay as they are. An empty list clears a field. |
| `cap` | | Monthly limit on matched mentions. Omit or null means none. |
| `groupId` | | The group to put it in (grp_...). Without it, the workspace's default group is used. Each group can hold a term only once. |
| `reviewSources` | | Review pages for this keyword, at most 10 (App Store and Google Play apps, Trustpilot pages, Google Maps places). Every new review on them is a mention, whatever it says. Pages are read daily, per country on the app stores. A newly added page imports its last 30 days, up to 100 newest reviews per country, free and without instant alerts. Later reviews are billed like other mentions. |
## Responses
| Status | Meaning |
| --- | --- |
| 201 | The new keyword |
| 401 | The API key is missing or not valid |
| 402 | The balance cannot cover one more keyword-day (insufficient_balance), or the workspace already has 500 keywords (keyword_limit_reached) |
| 409 | The group already has a keyword with this term after normalizing |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Delete a keyword
`DELETE /v1/keywords/{id}`
Deletes the keyword with its matches. A post that another keyword also matched is kept.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The keyword's id (kw_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 204 | Done |
| 401 | The API key is missing or not valid |
| 404 | No keyword with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Check a keyword's health
`GET /v1/keywords/{id}/health`
Checks whether the keyword earns its cost over `range` (default 30d). Returns a status (healthy, noisy, quiet, capped, paused or new) with plain-word reasons, numbers by platform and week, cost, the words and authors behind the noise, and suggestions. Send a suggestion's `patch` unchanged to PATCH /v1/keywords/{id}. Its effect comes from replaying the matcher's rules on the window's posts. `ai=true` adds a model-written context (cached a day, up to 20 model calls an hour per workspace). Read only and never billed. Reports are cached 5 minutes and rebuilt after a keyword change. Up to 30 reads a minute per workspace.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The keyword's id (kw_...). |
| `range` | query | How many UTC days back from today to read, by match time. One of 7d, 30d or 90d, 30d by default. |
| `ai` | query | true also asks a language model to rewrite the context. The answer is cached for a day per keyword and window, with at most 20 model calls an hour per workspace. With the default false, all suggestions come from the rules only. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The health report for the keyword |
| 400 | A query parameter is not valid |
| 401 | The API key is missing or not valid |
| 404 | No keyword with this id |
| 429 | Health was read over 30 times this minute (rate_limited) |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get a keyword
`GET /v1/keywords/{id}`
Returns one keyword with its matching rules, review pages, this month's cost and the polling status on each platform.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The keyword's id (kw_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The requested keyword |
| 401 | The API key is missing or not valid |
| 404 | No keyword with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List keywords
`GET /v1/keywords`
Returns the workspace's keywords with their match counts and polling status. With no parameters you get all keywords, latest first. `q` searches the term and context. `kind`, `status` and `platform` filter the list, `sort` orders it, and `limit` and `offset` page through it. `total` is the number of matching keywords before paging.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `q` | query | Case-insensitive search in the term and context. |
| `groupId` | query | Keeps keywords in these groups only (grp_...). Repeat the parameter or separate values with commas. |
| `kind` | query | Keeps keywords of these kinds only (brand, competitor, topic). Repeat the parameter or separate values with commas. |
| `status` | query | Keeps keywords in these states only (active, muted, paused, capped). Repeat the parameter or separate values with commas. |
| `platform` | query | Keeps keywords that run on one of these platforms. That means the term is searched there, which is every platform when platforms is null. For appstore and googleplay it means an app from that store is in reviewSources. Repeat the parameter or separate values with commas. |
| `sort` | query | newest puts the latest created first and oldest the earliest. term sorts A to Z. mentions puts the most matches first and relevant the most relevant matches. recent puts the most matches of the past 7 days first. lastMention puts the latest matched post first, with keywords that have none at the end. |
| `limit` | query | How many to return, from 1 to 500. Leave it out to get all keywords after `offset`. |
| `offset` | query | How many keywords to skip. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The keywords that match, with their total |
| 400 | A query parameter is not valid |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Update a keyword
`PATCH /v1/keywords/{id}`
Mutes or unmutes the keyword, or changes its `kind`, platforms, classifier `context`, `matching` rules or monthly mention `cap`. Each rule field is optional and an empty list clears it. A null cap removes the cap, and a cap above this month's count resumes a capped keyword right away. New rules apply to mentions from the next poll on. Stored mentions stay as they are.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The keyword's id (kw_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `kind` | | New kind (brand, competitor or topic). |
| `muted` | | Muting stops polling and matching. Existing mentions are kept. |
| `platforms` | | Sets a new platform list. Null means all platforms. An empty list means none, which works when the keyword has reviewSources and only collects reviews. |
| `context` | | Up to 300 characters the classifier reads for this keyword only, in addition to the company profile or the group's description. Say what the term means for you and what to ignore, for example "Driftwood is our deploy tool, not beach wood." Null clears it. |
| `matching` | | Fields you leave out stay as they are. An empty list clears a field. |
| `cap` | | Sets a new monthly mention cap, or null for no cap. A cap above this month's count resumes a capped keyword right away. A cap at or below the count pauses it. |
| `groupId` | | Target group (grp_...). 409 if it already has this term. |
| `reviewSources` | | Sets a new list of review pages for this keyword. An empty list disconnects them all, and their reviews are kept. A page or country you add gets the free 30-day look-back. Pages already listed are unchanged. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The keyword after the change |
| 401 | The API key is missing or not valid |
| 402 | The balance can't cover another keyword-day (insufficient_balance), or the workspace already runs 500 keywords (keyword_limit_reached) |
| 404 | No keyword with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Export mentions as CSV
`GET /v1/mentions/export.csv`
Returns as CSV the mentions GET /v1/mentions lists for the same filters. Rows are ordered by match time, latest first, which is the order they reached your feed and can differ from the post date. The columns are id, published_at, platform, keyword, author, author_url, author_followers, relevance, sentiment, intents (separated by |), language, confidence, status, relevant, delivered, url, links (separated by |), text (the first 1,000 characters), group, group_external_id, rating and app_id (reviews only), and title and image_url (on platforms that have them). The file holds at most 10,000 rows, and the X-Mentions-Truncated header tells you when rows were cut. Each workspace can export 6 times a minute, and a 429 includes Retry-After.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `keywordId` | query | Only this keyword's matches. |
| `platform` | query | Only this platform's posts. |
| `status` | query | Keeps mentions with this status only. Leave it out for all statuses. |
| `relevant` | query | true keeps mentions the classifier scored relevant. false keeps the others, including unscored ones. |
| `sentiment` | query | Keeps mentions with this sentiment only. |
| `intent` | query | Returns only mentions with this tag. One of bug_report, buy_intent, churn_intent, comparison, complaint, event, feedback, hiring, industry_insight, launch, praise, pricing, promotional, question and testimonial. |
| `automated` | query | true keeps mentions that look machine-made, such as bot accounts, scheduled or template posts and AI-written text. false keeps the others, including mentions scored before this flag existed. Leave it out for all. |
| `personId` | query | Keeps mentions by this person (an id from /v1/people), including merged accounts. Turns on includeMuted. |
| `includeMuted` | query | true also returns mentions by muted people, which are hidden by default. |
| `assigneeId` | query | Member user id. Returns only mentions assigned to them. |
| `snoozed` | query | true shows only snoozed mentions, which are otherwise hidden until they wake. |
| `excludeAuthors` | query | Leaves out these authors, given as display names, handles or profile links. Repeat the parameter or send one value with commas. |
| `minRelevance` | query | Keeps mentions with this relevance score or higher. Unscored mentions are left out. |
| `minConfidence` | query | Minimum classifier confidence, 0 to 1. Mentions with no confidence are dropped. |
| `minFollowers` | query | Sends only posts by authors with this many followers or more. Unknown counts are left out. |
| `maxFollowers` | query | Keeps posts by authors with this many followers or fewer. Unknown counts are left out. |
| `isReply` | query | true keeps replies and comments, meaning posts that answer another post. false keeps posts that are not replies. Leave it out for both. |
| `alertId` | query | Adds an alert rule's filter (an id from GET /v1/alerts) to the other filters. You get the mentions the rule would send, useful for a preview or an export in feed form. An unknown id returns 404. |
| `viewId` | query | Adds a saved view's filter (an id from GET /v1/views) to the other filters, with every condition joined by AND. You get exactly what the view shows. An unknown id returns 404. |
| `keywordKinds` | query | Keeps matches of keywords of these kinds only (brand, competitor, topic). Repeat the parameter or separate values with commas. |
| `tags` | query | Keeps authors your workspace gave one of these tags, matched exactly and by case. Repeat the parameter or separate values with commas. |
| `linkHosts` | query | Keeps posts that link to one of these hosts or its subdomains, so slack.com also matches api.slack.com. Repeat the parameter or separate values with commas. |
| `platforms` | query | Keeps posts from these platforms only. |
| `notPlatforms` | query | Platforms to exclude. |
| `keywordIds` | query | Keeps matches of these keywords only. |
| `groupIds` | query | Keeps matches of keywords in these groups only (grp_...). Repeat the parameter or separate values with commas. |
| `notGroupIds` | query | Hides matches from keywords that belong to these groups. |
| `notKeywordIds` | query | Keyword ids to exclude. |
| `sentiments` | query | Keeps mentions with these sentiments only. |
| `notSentiments` | query | Leaves out these sentiments. Mentions not scored yet are kept. |
| `intents` | query | Keeps mentions tagged with one of these intents or topics. |
| `notIntents` | query | Intent or topic tags to exclude. |
| `notLinkHosts` | query | Leaves out posts that link to these hosts or their subdomains. |
| `notTags` | query | Leaves out authors your workspace gave one of these tags. |
| `languages` | query | Sends only posts in these languages, as ISO 639-1 codes such as en, es or de. Posts with an unknown language are left out. |
| `notLanguages` | query | Leaves out posts in these languages. Posts with an unknown language are kept. |
| `ratings` | query | Keeps reviews with one of these star ratings, from 1 to 5. Use ratings=1,2 for the unhappy ones. Posts that are not reviews are left out. |
| `notRatings` | query | Star ratings (1 to 5) to hide, such as notRatings=5. Other posts are unaffected. |
| `minLikes` | query | Keeps posts with this many likes (upvotes, reactions) or more, counted when the post was collected. Posts without a like count are left out. |
| `minReposts` | query | Keeps posts with this many reposts (shares, retweets) or more, counted when the post was collected. Posts without a repost count are left out. |
| `minReplies` | query | Keeps posts with this many replies (comments) or more, counted when the post was collected. Posts without a reply count are left out. |
| `minQuotes` | query | Keeps posts with this many quotes or more, counted when the post was collected. Posts without a quote count are left out. |
| `minViews` | query | Keeps posts with this many views (plays) or more, counted when the post was collected. Posts without a view count are left out. |
| `minBookmarks` | query | Keeps posts with this many bookmarks (saves) or more, counted when the post was collected. Posts without a bookmark count are left out. |
| `anyOf` | query | Groups of conditions joined by OR, as URL-encoded JSON. For example [{"platforms":["github"],"intents":["bug_report"]},{"sentiments":["negative"]}] means "bug reports on GitHub, or anything negative". A group uses the fields of a view filter, where a list matches any value, a not list matches none, and all conditions are joined by AND. A mention passes when one group matches, and the other filters here still apply. Send 1 to 10 groups, none empty and none nested. |
| `q` | query | Searches post text and author names. |
| `since` | query | Keeps posts published at this time or later, as ISO 8601 or epoch ms. |
| `until` | query | Keeps posts published at this time or earlier, as ISO 8601 or epoch ms. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | A UTF-8 CSV file |
| 400 | A query parameter is not valid |
| 401 | The API key is missing or not valid |
| 429 | Over 6 exports in this minute. Wait the Retry-After seconds and try again |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Export mentions as JSON
`GET /v1/mentions/export.json`
Returns in one response the mentions GET /v1/mentions lists for the same filters, latest match first, which is the order they reached your feed. Each row is the full Mention object from the list, text included. At most 10,000 mentions are returned, and `truncated` tells you when some were cut. It shares the CSV export's limit of 6 exports a minute per workspace, in either format, and a 429 includes Retry-After.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `keywordId` | query | Only this keyword's matches. |
| `platform` | query | Only this platform's posts. |
| `status` | query | Keeps mentions with this status only. Leave it out for all statuses. |
| `relevant` | query | true keeps mentions the classifier scored relevant. false keeps the others, including unscored ones. |
| `sentiment` | query | Keeps mentions with this sentiment only. |
| `intent` | query | Returns only mentions with this tag. One of bug_report, buy_intent, churn_intent, comparison, complaint, event, feedback, hiring, industry_insight, launch, praise, pricing, promotional, question and testimonial. |
| `automated` | query | true keeps mentions that look machine-made, such as bot accounts, scheduled or template posts and AI-written text. false keeps the others, including mentions scored before this flag existed. Leave it out for all. |
| `personId` | query | Keeps mentions by this person (an id from /v1/people), including merged accounts. Turns on includeMuted. |
| `includeMuted` | query | true also returns mentions by muted people, which are hidden by default. |
| `assigneeId` | query | Member user id. Returns only mentions assigned to them. |
| `snoozed` | query | true shows only snoozed mentions, which are otherwise hidden until they wake. |
| `excludeAuthors` | query | Leaves out these authors, given as display names, handles or profile links. Repeat the parameter or send one value with commas. |
| `minRelevance` | query | Keeps mentions with this relevance score or higher. Unscored mentions are left out. |
| `minConfidence` | query | Minimum classifier confidence, 0 to 1. Mentions with no confidence are dropped. |
| `minFollowers` | query | Sends only posts by authors with this many followers or more. Unknown counts are left out. |
| `maxFollowers` | query | Keeps posts by authors with this many followers or fewer. Unknown counts are left out. |
| `isReply` | query | true keeps replies and comments, meaning posts that answer another post. false keeps posts that are not replies. Leave it out for both. |
| `alertId` | query | Adds an alert rule's filter (an id from GET /v1/alerts) to the other filters. You get the mentions the rule would send, useful for a preview or an export in feed form. An unknown id returns 404. |
| `viewId` | query | Adds a saved view's filter (an id from GET /v1/views) to the other filters, with every condition joined by AND. You get exactly what the view shows. An unknown id returns 404. |
| `keywordKinds` | query | Keeps matches of keywords of these kinds only (brand, competitor, topic). Repeat the parameter or separate values with commas. |
| `tags` | query | Keeps authors your workspace gave one of these tags, matched exactly and by case. Repeat the parameter or separate values with commas. |
| `linkHosts` | query | Keeps posts that link to one of these hosts or its subdomains, so slack.com also matches api.slack.com. Repeat the parameter or separate values with commas. |
| `platforms` | query | Keeps posts from these platforms only. |
| `notPlatforms` | query | Platforms to exclude. |
| `keywordIds` | query | Keeps matches of these keywords only. |
| `groupIds` | query | Keeps matches of keywords in these groups only (grp_...). Repeat the parameter or separate values with commas. |
| `notGroupIds` | query | Hides matches from keywords that belong to these groups. |
| `notKeywordIds` | query | Keyword ids to exclude. |
| `sentiments` | query | Keeps mentions with these sentiments only. |
| `notSentiments` | query | Leaves out these sentiments. Mentions not scored yet are kept. |
| `intents` | query | Keeps mentions tagged with one of these intents or topics. |
| `notIntents` | query | Intent or topic tags to exclude. |
| `notLinkHosts` | query | Leaves out posts that link to these hosts or their subdomains. |
| `notTags` | query | Leaves out authors your workspace gave one of these tags. |
| `languages` | query | Sends only posts in these languages, as ISO 639-1 codes such as en, es or de. Posts with an unknown language are left out. |
| `notLanguages` | query | Leaves out posts in these languages. Posts with an unknown language are kept. |
| `ratings` | query | Keeps reviews with one of these star ratings, from 1 to 5. Use ratings=1,2 for the unhappy ones. Posts that are not reviews are left out. |
| `notRatings` | query | Star ratings (1 to 5) to hide, such as notRatings=5. Other posts are unaffected. |
| `minLikes` | query | Keeps posts with this many likes (upvotes, reactions) or more, counted when the post was collected. Posts without a like count are left out. |
| `minReposts` | query | Keeps posts with this many reposts (shares, retweets) or more, counted when the post was collected. Posts without a repost count are left out. |
| `minReplies` | query | Keeps posts with this many replies (comments) or more, counted when the post was collected. Posts without a reply count are left out. |
| `minQuotes` | query | Keeps posts with this many quotes or more, counted when the post was collected. Posts without a quote count are left out. |
| `minViews` | query | Keeps posts with this many views (plays) or more, counted when the post was collected. Posts without a view count are left out. |
| `minBookmarks` | query | Keeps posts with this many bookmarks (saves) or more, counted when the post was collected. Posts without a bookmark count are left out. |
| `anyOf` | query | Groups of conditions joined by OR, as URL-encoded JSON. For example [{"platforms":["github"],"intents":["bug_report"]},{"sentiments":["negative"]}] means "bug reports on GitHub, or anything negative". A group uses the fields of a view filter, where a list matches any value, a not list matches none, and all conditions are joined by AND. A mention passes when one group matches, and the other filters here still apply. Send 1 to 10 groups, none empty and none nested. |
| `q` | query | Searches post text and author names. |
| `since` | query | Keeps posts published at this time or later, as ISO 8601 or epoch ms. |
| `until` | query | Keeps posts published at this time or earlier, as ISO 8601 or epoch ms. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | All mentions the filter selects, 10,000 at most |
| 400 | A query parameter is not valid |
| 401 | The API key is missing or not valid |
| 429 | Over 6 exports in this minute. Wait the Retry-After seconds and try again |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get a mention
`GET /v1/mentions/{id}`
Fetches one mention by id, with the same fields the list returns. An id from another workspace gets a 404.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | Mention id (mm_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The mention |
| 401 | The API key is missing or not valid |
| 404 | Mention not found |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List mentions
`GET /v1/mentions`
Lists your keywords' mentions with filters and paging. Each mention pairs one post with one keyword. Latest match first by default, or sort=priority to rank the last 30 days by attention score. For the next page, send nextCursor and keep the filters and sort. alertId applies an alert's filter, giving what that alert would send. anyOf adds OR groups as URL-encoded JSON, at least one of which must match along with every other filter.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `keywordId` | query | Only this keyword's matches. |
| `platform` | query | Only this platform's posts. |
| `status` | query | Keeps mentions with this status only. Leave it out for all statuses. |
| `relevant` | query | true keeps mentions the classifier scored relevant. false keeps the others, including unscored ones. |
| `sentiment` | query | Keeps mentions with this sentiment only. |
| `intent` | query | Returns only mentions with this tag. One of bug_report, buy_intent, churn_intent, comparison, complaint, event, feedback, hiring, industry_insight, launch, praise, pricing, promotional, question and testimonial. |
| `automated` | query | true keeps mentions that look machine-made, such as bot accounts, scheduled or template posts and AI-written text. false keeps the others, including mentions scored before this flag existed. Leave it out for all. |
| `personId` | query | Keeps mentions by this person (an id from /v1/people), including merged accounts. Turns on includeMuted. |
| `includeMuted` | query | true also returns mentions by muted people, which are hidden by default. |
| `assigneeId` | query | Member user id. Returns only mentions assigned to them. |
| `snoozed` | query | true shows only snoozed mentions, which are otherwise hidden until they wake. |
| `excludeAuthors` | query | Leaves out these authors, given as display names, handles or profile links. Repeat the parameter or send one value with commas. |
| `minRelevance` | query | Keeps mentions with this relevance score or higher. Unscored mentions are left out. |
| `minConfidence` | query | Minimum classifier confidence, 0 to 1. Mentions with no confidence are dropped. |
| `minFollowers` | query | Sends only posts by authors with this many followers or more. Unknown counts are left out. |
| `maxFollowers` | query | Keeps posts by authors with this many followers or fewer. Unknown counts are left out. |
| `isReply` | query | true keeps replies and comments, meaning posts that answer another post. false keeps posts that are not replies. Leave it out for both. |
| `alertId` | query | Adds an alert rule's filter (an id from GET /v1/alerts) to the other filters. You get the mentions the rule would send, useful for a preview or an export in feed form. An unknown id returns 404. |
| `viewId` | query | Adds a saved view's filter (an id from GET /v1/views) to the other filters, with every condition joined by AND. You get exactly what the view shows. An unknown id returns 404. |
| `keywordKinds` | query | Keeps matches of keywords of these kinds only (brand, competitor, topic). Repeat the parameter or separate values with commas. |
| `tags` | query | Keeps authors your workspace gave one of these tags, matched exactly and by case. Repeat the parameter or separate values with commas. |
| `linkHosts` | query | Keeps posts that link to one of these hosts or its subdomains, so slack.com also matches api.slack.com. Repeat the parameter or separate values with commas. |
| `platforms` | query | Keeps posts from these platforms only. |
| `notPlatforms` | query | Platforms to exclude. |
| `keywordIds` | query | Keeps matches of these keywords only. |
| `groupIds` | query | Keeps matches of keywords in these groups only (grp_...). Repeat the parameter or separate values with commas. |
| `notGroupIds` | query | Hides matches from keywords that belong to these groups. |
| `notKeywordIds` | query | Keyword ids to exclude. |
| `sentiments` | query | Keeps mentions with these sentiments only. |
| `notSentiments` | query | Leaves out these sentiments. Mentions not scored yet are kept. |
| `intents` | query | Keeps mentions tagged with one of these intents or topics. |
| `notIntents` | query | Intent or topic tags to exclude. |
| `notLinkHosts` | query | Leaves out posts that link to these hosts or their subdomains. |
| `notTags` | query | Leaves out authors your workspace gave one of these tags. |
| `languages` | query | Sends only posts in these languages, as ISO 639-1 codes such as en, es or de. Posts with an unknown language are left out. |
| `notLanguages` | query | Leaves out posts in these languages. Posts with an unknown language are kept. |
| `ratings` | query | Keeps reviews with one of these star ratings, from 1 to 5. Use ratings=1,2 for the unhappy ones. Posts that are not reviews are left out. |
| `notRatings` | query | Star ratings (1 to 5) to hide, such as notRatings=5. Other posts are unaffected. |
| `minLikes` | query | Keeps posts with this many likes (upvotes, reactions) or more, counted when the post was collected. Posts without a like count are left out. |
| `minReposts` | query | Keeps posts with this many reposts (shares, retweets) or more, counted when the post was collected. Posts without a repost count are left out. |
| `minReplies` | query | Keeps posts with this many replies (comments) or more, counted when the post was collected. Posts without a reply count are left out. |
| `minQuotes` | query | Keeps posts with this many quotes or more, counted when the post was collected. Posts without a quote count are left out. |
| `minViews` | query | Keeps posts with this many views (plays) or more, counted when the post was collected. Posts without a view count are left out. |
| `minBookmarks` | query | Keeps posts with this many bookmarks (saves) or more, counted when the post was collected. Posts without a bookmark count are left out. |
| `anyOf` | query | Groups of conditions joined by OR, as URL-encoded JSON. For example [{"platforms":["github"],"intents":["bug_report"]},{"sentiments":["negative"]}] means "bug reports on GitHub, or anything negative". A group uses the fields of a view filter, where a list matches any value, a not list matches none, and all conditions are joined by AND. A mention passes when one group matches, and the other filters here still apply. Send 1 to 10 groups, none empty and none nested. |
| `q` | query | Searches post text and author names. |
| `since` | query | Keeps posts published at this time or later, as ISO 8601 or epoch ms. |
| `until` | query | Keeps posts published at this time or earlier, as ISO 8601 or epoch ms. |
| `sort` | query | newest orders by match time, latest first. priority orders by attention score, highest first, and only covers the past 30 days of matches. Older ones are still available with newest. A cursor only works with the sort it came from. |
| `cursor` | query | Pass the previous page's nextCursor, with the same filters and sort. |
| `limit` | query | How many to return, from 1 to 100. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | A page of mentions, sorted as requested |
| 400 | A query parameter or the cursor is not valid |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Update a mention
`PATCH /v1/mentions/{id}`
The only way to change a mention. Set status to ignored or done when it is handled, or back to open. You can also assign it to a member, snooze it out of the feed, add a note for your team, or correct the classifier. `relevant` true or false is your judgment, which sets relevance to 100 or 0 for every list, filter, digest and report. `sentiment` replaces the label. Null removes your correction and brings back the classifier's value. Fields you leave out stay as they are. Delivery and billing are never affected.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | Mention id (mm_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `status` | | ignored or done marks it handled, and open reopens it. |
| `assigneeId` | | The user id of a workspace member, or null to remove the assignment. |
| `snoozedUntil` | | Hides the mention from the feed until this time, given as ISO 8601 or epoch ms. Send null to bring it back now. |
| `note` | | A note for your team. Null or an empty string removes it. |
| `relevant` | | Set it to override the classifier, or null to restore the classifier's score. true means relevance 100, and a filtered mention joins the relevant feed. false means 0, and it leaves. Billing is unaffected. While the mention is still being classified this returns 409 classification_pending. |
| `sentiment` | | The sentiment you choose. Null removes it and brings back the classifier's. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The updated mention |
| 400 | assigneeId isn't a workspace member |
| 401 | The API key is missing or not valid |
| 404 | Mention not found |
| 409 | The classifier has not scored this mention yet (classification_pending) |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get the company profile
`GET /v1/company`
Returns what the classifier knows about your company. That is the name, description, use cases and your accounts, plus the context text built from them.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Your company profile |
| 401 | The API key is missing or not valid |
| 404 | The workspace does not exist |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Update the company profile
`PATCH /v1/company`
Changing profile fields rebuilds the classifier's context. Setting `context` yourself replaces it until the next profile change. New mentions are scored with the change right away.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `name` | | |
| `description` | | |
| `useCases` | | Sets the full new list. |
| `accounts` | | |
| `website` | | The company's website. Null removes it. |
| `competitors` | | Sets the full new list. An empty list clears it. |
| `guidelines` | | Rules for the classifier in your own words. Null removes them. |
| `context` | | Replaces the built context until the next profile change. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The profile after the change |
| 401 | The API key is missing or not valid |
| 404 | The workspace does not exist |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Create an API key
`POST /v1/api-keys`
Issues a new API key in this workspace. The key is shown only in this response, and only its hash is stored. `expiresAt` makes it stop working at a set time, useful for a contractor or a one-off script. An expired key stays in the list until you revoke it.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `name` | | A name for the key. Defaults to "default". |
| `scope` | | write can do everything. read is limited to GET. |
| `expiresAt` | | Expiry time in ISO 8601 or epoch ms, later than now. Set it for keys you hand to a script or contractor. Omit it or send null and the key never expires. |
## Responses
| Status | Meaning |
| --- | --- |
| 201 | The created key, visible only now |
| 400 | expiresAt must be a future time |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List API keys
`GET /v1/api-keys`
Lists the workspace's active keys, latest first, with name, prefix, scope, expiry and last use. Revoked keys and secrets are never listed.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | All keys, latest first, without the secret |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Revoke an API key
`DELETE /v1/api-keys/{id}`
Revokes the key. The API refuses it right away, and cached checks catch up within a few minutes.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The API key's id (key_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 204 | The key no longer works |
| 401 | The API key is missing or not valid |
| 404 | No API key with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get Health
`GET /v1/health`
Checks that the API is up. Needs no key and is not rate limited. Returns `ok: true` with the state of the database and each background worker.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The API is running |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Create an alert
`POST /v1/alerts`
Creates an alert, which pairs a filter with one or more channels. instant mode sends each matching mention as it arrives. hourly sends a digest of the previous full UTC hour at five past, skips hours with no mention above the rule's minimum, takes no schedule and works with Slack, Telegram and webhook channels only. daily sends one digest at schedule.hour in schedule.timezone. weekly sends one a week on schedule.weekday, from 0 for Sunday to 6 for Saturday. filter.anyOf adds OR logic with groups of mention-list conditions. At least one group must match, as well as the rest of the filter.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `name` | yes | |
| `enabled` | | |
| `mode` | | |
| `filter` | | |
| `schedule` | | Daily and weekly alerts need it, and weekly ones need schedule.weekday too. Hourly alerts send every UTC hour, ignore a time or zone, and reject a weekday. |
| `event` | | Custom webhook event name. Null keeps the mode's default. |
| `channelIds` | | Where to send, as ids from GET /v1/channels. |
## Responses
| Status | Meaning |
| --- | --- |
| 201 | The new alert |
| 400 | The schedule doesn't fit the mode, an hourly alert uses email (hourly_email_unsupported), or a channel id is unknown |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Create a channel
`POST /v1/channels`
Creates a webhook (the default), email or Slack channel. Slack needs a connected workspace, otherwise 409. Telegram chats are connected in the dashboard. A webhook's signing secret appears only in this response.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `kind` | | |
| `channelId` | | The id of a channel in the connected Slack workspace. |
| `channelName` | | The Slack channel's name, used as the label. |
| `events` | | Account events to send to this endpoint, in addition to what rules send. These are keyword and wallet changes and attention events. Leave it out for none. |
| `emails` | | Each address gets a link to confirm it. Workspace members are confirmed right away. |
| `url` | | The URL that receives the signed POST requests. Must be https in production. |
| `label` | | A name for the channel. Defaults to the URL's host. |
| `headers` | | Additional headers to send with each request, such as your own auth. |
## Responses
| Status | Meaning |
| --- | --- |
| 201 | Created. For a webhook this is the only time the signing secret is shown. |
| 400 | The channel kind doesn't support one of the events. Slack, email and Telegram accept attention events only |
| 401 | The API key is missing or not valid |
| 409 | This workspace has not connected Slack |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Delete an alert
`DELETE /v1/alerts/{id}`
Deletes the alert and its delivery history. Its channels stay.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The alert's id (feed_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 204 | Done |
| 401 | The API key is missing or not valid |
| 404 | No alert with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Delete a channel
`DELETE /v1/channels/{id}`
Deletes the channel and takes it off its alerts. Queued sends to it are cancelled. The alerts stay.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The channel's id (dest_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 204 | Done |
| 401 | The API key is missing or not valid |
| 404 | No channel with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get an alert
`GET /v1/alerts/{id}`
Returns one alert with its filter, mode, schedule, channels and delivery stats.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The alert's id (feed_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The requested alert |
| 401 | The API key is missing or not valid |
| 404 | No alert with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get a channel
`GET /v1/channels/{id}`
Returns one channel with its settings, account events and delivery stats. A webhook's signing secret is never shown here.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The channel's id (dest_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The requested channel |
| 401 | The API key is missing or not valid |
| 404 | No channel with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List alerts
`GET /v1/alerts`
Lists the workspace's alerts, latest first, each with its filter, mode, schedule and channels.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | All of the workspace's alerts, latest first |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List a channel's deliveries
`GET /v1/channels/{id}/deliveries`
Lists the mentions, digests and account events sent to the channel, latest first, with status and the last error. `limit` defaults to 50, up to 200.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The channel's id (dest_...). |
| `limit` | query | |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Everything sent to the channel, latest first, including instant mentions and digests, with status and the last error |
| 401 | The API key is missing or not valid |
| 404 | No channel with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List channels
`GET /v1/channels`
Lists the workspace's channels with their settings and delivery stats.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | All of the workspace's channels |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Mute authors
`POST /v1/alerts/{id}/mute`
Adds authors to the alert's muted list and leaves the rest of its filter alone. Links are handled as in the dashboard, so a post link mutes its author, twitter.com turns into x.com and a Hacker News profile keeps its id. Authors who are already muted are skipped, so retrying is safe. An entry that is not a person, such as a subreddit or a story, fails the request and the error names it.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The alert's id (feed_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `authors` | yes | A profile link (x.com/name, linkedin.com/in/name, reddit.com/user/name), post link, handle (@name, u/name), Bluesky DID or display name. Links are stored as the author's profile. A bare name or handle covers every platform. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Updated alert with the new muted list |
| 400 | An entry is not a person, or the alert would have over 200 muted authors |
| 401 | The API key is missing or not valid |
| 404 | No alert with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Rotate the signing secret
`POST /v1/channels/{id}/rotate-secret`
Gives a webhook channel a new signing secret, shown only in this response. The old secret stops working at once.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The channel's id (dest_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The new signing secret, shown only now. The old one stops working right away. |
| 401 | The API key is missing or not valid |
| 404 | No webhook channel with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Send a digest now
`POST /v1/alerts/{id}/run`
Sends the digest for the alert's last hour, day or week now and returns each channel's outcome. Works on digest alerts only, and the schedule doesn't change.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The alert's id (feed_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Sent now for the alert's last hour, day or week. The scheduled digest is unaffected |
| 400 | Digests exist only for hourly, daily and weekly alerts |
| 401 | The API key is missing or not valid |
| 404 | No alert with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Test an alert
`POST /v1/alerts/{id}/test`
Sends a test message to each of the alert's channels now and returns each outcome. Webhooks receive the event `test`.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The alert's id (feed_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | A test was tried on each of the alert's channels |
| 401 | The API key is missing or not valid |
| 404 | No alert with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Test a channel
`POST /v1/channels/{id}/test`
Sends a test message to the channel now and returns the outcome. Webhooks receive the event `test`.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The channel's id (dest_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Test attempt made. Webhooks receive event "test" |
| 401 | The API key is missing or not valid |
| 404 | No channel with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Unmute authors
`POST /v1/alerts/{id}/unmute`
Takes authors off the muted list, keeping the rest of the filter. Each entry can be the stored value or any link to the person's profile or posts. Retrying is safe, because authors who aren't muted are skipped.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The alert's id (feed_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `authors` | yes | A profile link (x.com/name, linkedin.com/in/name, reddit.com/user/name), post link, handle (@name, u/name), Bluesky DID or display name. Links are stored as the author's profile. A bare name or handle covers every platform. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Updated alert with the new muted list |
| 401 | The API key is missing or not valid |
| 404 | No alert with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Update an alert
`PATCH /v1/alerts/{id}`
Changes an alert. `filter` and `channelIds` replace the old values. Queued digests are cancelled, as are queued sends to a removed channel or from a disabled alert.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The alert's id (feed_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `name` | | |
| `enabled` | | |
| `mode` | | |
| `filter` | | Sets the full new filter. |
| `schedule` | | For daily and weekly alerts. An hourly alert ignores it, but refuses a weekday. |
| `event` | | |
| `channelIds` | | Sets the full new list. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The alert after the change |
| 400 | The schedule doesn't fit the mode, an hourly alert uses email (hourly_email_unsupported), or a channel id is unknown |
| 401 | The API key is missing or not valid |
| 404 | No alert with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Update a channel
`PATCH /v1/channels/{id}`
Changes a channel's label and account events, and a webhook's URL and headers. `headers` and `events` replace the old values.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The channel's id (dest_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `label` | | |
| `url` | | For webhook channels only. |
| `headers` | | For webhook channels only. Sets the full new set of headers. |
| `events` | | Sets the full list of account events the channel gets. A webhook can take any of them. Slack, email and Telegram channels take only the attention events (mention.spike, sentiment.negative_spike, keyword.noisy, channel.failing). An empty list turns them all off. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The channel after the change |
| 400 | The channel kind doesn't support one of the events. Slack, email and Telegram accept attention events only |
| 401 | The API key is missing or not valid |
| 404 | No channel with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get a mention breakdown
`GET /v1/analytics/breakdown`
`by` sets the rows (platform, keyword, sentiment, intent, status, hour as weekday and hour, or person), and each row has matched, relevant and sentiment counts. Pick the period with `range` (7d, 30d, 90d or 365d up to today) or with `from` and `to`. Days follow `timezone`, UTC by default. `keywordIds` and `platforms` narrow the data, and `compare=true` adds the equally long period just before. Dates are publish dates.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `range` | query | A preset period up to today, 30d by default. Ignored when you send from or to. |
| `from` | query | Start date in `timezone`, as YYYY-MM-DD. That day is included. |
| `to` | query | End date in `timezone`, as YYYY-MM-DD, included. Defaults to today. |
| `keywordIds` | query | Keeps these keyword ids only. Repeat the parameter or separate values with commas. Leave it out for all keywords. |
| `platforms` | query | Keeps these platforms only. Repeat the parameter or separate values with commas. Leave it out for all platforms. |
| `compare` | query | true adds the equally long period just before as `previous`. |
| `timezone` | query | The IANA time zone used to split days, such as America/New_York. Defaults to UTC. The zone's offset at the end of the period is used for the whole period. |
| `by` | query, required | How to group the rows. platform, keyword, sentiment (unclassified included), intent (a mention can have several), status (open, ignored, done), hour (weekday and hour in `timezone`), person (the author, without anonymous posts) or language (ISO 639-1, or "unknown" when there is none). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The rows, the one with the most matches first |
| 400 | The query is not valid |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Mentions over time
`GET /v1/analytics/series`
Returns matched, relevant and sentiment counts per day or week over the period. You get one total series, or one per platform or keyword with `by`. Pick the period with `range` (7d, 30d, 90d or 365d up to today) or with `from` and `to`. Days follow `timezone`, UTC by default. `keywordIds` and `platforms` narrow the data, and `compare=true` adds the equally long period just before. Dates are publish dates.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `range` | query | A preset period up to today, 30d by default. Ignored when you send from or to. |
| `from` | query | Start date in `timezone`, as YYYY-MM-DD. That day is included. |
| `to` | query | End date in `timezone`, as YYYY-MM-DD, included. Defaults to today. |
| `keywordIds` | query | Keeps these keyword ids only. Repeat the parameter or separate values with commas. Leave it out for all keywords. |
| `platforms` | query | Keeps these platforms only. Repeat the parameter or separate values with commas. Leave it out for all platforms. |
| `compare` | query | true adds the equally long period just before as `previous`. |
| `timezone` | query | The IANA time zone used to split days, such as America/New_York. Defaults to UTC. The zone's offset at the end of the period is used for the whole period. |
| `bucket` | query | The size of each point. hour for periods up to 14 days, day, week (starting Monday) or month. By default days up to 90 days and weeks beyond. |
| `by` | query | Splits the data into a series for each platform, each keyword or each sentiment (positive, neutral, negative, unclassified). By keyword you get the top 20 by matches, with the rest combined as "other". Leave it out for one total series. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The series with zeros for empty points, oldest first |
| 400 | The query is not valid |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get summary counts
`GET /v1/analytics/summary`
Returns matched and relevant mentions, unique posts and people, sentiment, buying intent and questions, estimated reach, and the triage status of the matches. Pick the period with `range` (7d, 30d, 90d or 365d up to today) or with `from` and `to`. Days follow `timezone`, UTC by default. `keywordIds` and `platforms` narrow the data, and `compare=true` adds the equally long period just before. Dates are publish dates.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `range` | query | A preset period up to today, 30d by default. Ignored when you send from or to. |
| `from` | query | Start date in `timezone`, as YYYY-MM-DD. That day is included. |
| `to` | query | End date in `timezone`, as YYYY-MM-DD, included. Defaults to today. |
| `keywordIds` | query | Keeps these keyword ids only. Repeat the parameter or separate values with commas. Leave it out for all keywords. |
| `platforms` | query | Keeps these platforms only. Repeat the parameter or separate values with commas. Leave it out for all platforms. |
| `compare` | query | true adds the equally long period just before as `previous`. |
| `timezone` | query | The IANA time zone used to split days, such as America/New_York. Defaults to UTC. The zone's offset at the end of the period is used for the whole period. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The counts, plus `previous` with compare=true |
| 400 | The query is not valid |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get review stats
`GET /v1/analytics/reviews`
Returns stats on reviews your keywords collect from the App Store, Google Play, Trustpilot and Google Maps. You get the count, average stars, the star distribution, replies and open 1 and 2 star reviews, for the workspace and per review page. Each page has a series of average stars per `bucket`, and the report lists the tags of unhappy reviews. Two keywords matching one review count it once. Pick the period with `range` (7d, 30d, 90d or 365d up to today) or with `from` and `to`. Days follow `timezone`, UTC by default. `keywordIds` and `platforms` narrow the data, and `compare=true` adds the equally long period just before. Dates are publish dates.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `range` | query | A preset period up to today, 30d by default. Ignored when you send from or to. |
| `from` | query | Start date in `timezone`, as YYYY-MM-DD. That day is included. |
| `to` | query | End date in `timezone`, as YYYY-MM-DD, included. Defaults to today. |
| `keywordIds` | query | Keeps these keyword ids only. Repeat the parameter or separate values with commas. Leave it out for all keywords. |
| `platforms` | query | Keeps these platforms only. Repeat the parameter or separate values with commas. Leave it out for all platforms. |
| `compare` | query | true adds the equally long period just before as `previous`. |
| `timezone` | query | The IANA time zone used to split days, such as America/New_York. Defaults to UTC. The zone's offset at the end of the period is used for the whole period. |
| `bucket` | query | The step of each series, day or week. Days by default for periods up to 90 days. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The report, plus `previous` with compare=true |
| 400 | The query is not valid |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get share of voice
`GET /v1/analytics/share-of-voice`
Returns each keyword with matches in the period, its counts and its share of all brand and competitor matches. Topic keywords are counted but not part of the split. Pick the period with `range` (7d, 30d, 90d or 365d up to today) or with `from` and `to`. Days follow `timezone`, UTC by default. `keywordIds` and `platforms` narrow the data, and `compare=true` adds the equally long period just before. Dates are publish dates.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `range` | query | A preset period up to today, 30d by default. Ignored when you send from or to. |
| `from` | query | Start date in `timezone`, as YYYY-MM-DD. That day is included. |
| `to` | query | End date in `timezone`, as YYYY-MM-DD, included. Defaults to today. |
| `keywordIds` | query | Keeps these keyword ids only. Repeat the parameter or separate values with commas. Leave it out for all keywords. |
| `platforms` | query | Keeps these platforms only. Repeat the parameter or separate values with commas. Leave it out for all platforms. |
| `compare` | query | true adds the equally long period just before as `previous`. |
| `timezone` | query | The IANA time zone used to split days, such as America/New_York. Defaults to UTC. The zone's offset at the end of the period is used for the whole period. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The keywords, the one with the most matches first |
| 400 | The query is not valid |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Dismiss an attention item
`POST /v1/attention/{id}/dismiss`
Dismisses an item. It leaves the open list and stays away while its condition lasts. After the condition ends, a new episode can open a new item. Calling it twice has the same effect as once.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The attention item's id (att_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The item, now dismissed |
| 401 | The API key is missing or not valid |
| 404 | This workspace has no attention item with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List attention items
`GET /v1/attention`
Lists items that need a person, latest first. Kinds are mention.spike (a keyword's mentions jumped in the last hour), sentiment.negative_spike (its 24-hour negative share jumped), keyword.noisy (it turned noisy) and channel.failing (a channel's recent sends all failed). Checks run hourly. Items open when the condition starts and resolve when it ends. Open items are the default, and `status=all` adds the history. Opening an item also sends an account event of the same name to subscribed webhook, Slack, email and Telegram channels.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `status` | query | Which items to list. Defaults to open. |
| `kind` | query | Filters by kind. Send a comma-separated list such as mention.spike,keyword.noisy. |
| `limit` | query | How many items to return, latest first. 50 by default and 100 at most. |
| `cursor` | query | Pass the previous page's nextCursor. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The attention items |
| 400 | The kind is unknown, or the cursor did not come from this list |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Check the credential
`GET /v1/whoami`
Call this first. It shows the workspace the credential works in, its type (API key, MCP sign-in token or dashboard session), whether it can write, and for keys the id and expiry. A read key gets 403 read_only_key on any write, and scripts often aim at the wrong workspace.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Details of the credential |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Start a top-up
`POST /v1/billing/top-ups`
Returns a hosted checkout link with `amountCents` filled in. The buyer can change it there, from $20 to $5,000. The balance is credited within a minute of payment, and tracking paused for balance resumes right away. This call charges nothing by itself. `successUrl` must be on an origin this deployment trusts. Leave it out to return to the dashboard's billing page.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `amountCents` | yes | The amount to add in US cents, from 2000 to 500000. Checkout starts with it and the buyer can change it. |
| `successUrl` | | Page to return to after payment. It must be on an origin this deployment trusts, like the dashboard, and defaults to the dashboard's billing page. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | A checkout link with the amount filled in |
| 400 | The amount is not valid, or successUrl is not allowed |
| 401 | The credential is missing or not valid |
| 404 | There is no workspace owner to bill |
| 429 | The workspace started too many checkouts in this minute |
| 503 | This deployment has no billing set up |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get a receipt link
`GET /v1/billing/invoices/{id}/url`
For a paid order from GET /v1/billing/invoices, returns a receipt PDF URL that expires soon.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | Which paid order. Ids come from GET /v1/billing/invoices. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Receipt PDF URL, valid briefly |
| 401 | The credential is missing or not valid |
| 404 | The receipt does not exist, or its PDF is not ready |
| 503 | No billing is set up |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get the balance
`GET /v1/billing/wallet`
Returns every detail of the prepaid balance. That covers the ledger total, pending mention charges, the effective balance used for pausing, the daily spend and days left, running and paused keywords, the cost of a day and of resuming, the welcome credit, the latest top-up, the top-up limits and the auto-recharge settings. `GET /v1/usage` gives a shorter version.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Wallet balance, daily spend and pause state |
| 401 | The credential is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List receipts
`GET /v1/billing/invoices`
Returns the top-up orders, latest first, as Polar, the merchant of record, stores them. You get this workspace's orders among the billing customer's latest 100. No orders exist before the first top-up.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Receipts for paid orders. Empty before the first payment |
| 401 | The credential is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List ledger entries
`GET /v1/billing/ledger`
Shows the balance's history, latest first. Entries cover the welcome credit, top-ups, refunds, adjustments, and the daily keyword-day and mention debits. Debits carry their UTC settlement day and cumulative units. Cursor pages.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `cursor` | query | The `nextCursor` of the previous page. Do not parse it. |
| `limit` | query | How many to return, from 1 to 100, 25 by default. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | Ledger entries, latest first |
| 400 | The cursor is not valid |
| 401 | The credential is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get the workspace filters
`GET /v1/filters`
Returns the workspace noise rules, applied to every keyword before a mention is stored. They exclude terms, authors and GitHub repositories, and allow or block subreddits. Rejected posts are never classified, sent or billed. Keywords add their own `matching` rules on top.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The workspace's filters |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Update the workspace filters
`PATCH /v1/filters`
Sets new values for any of the lists. A list you leave out stays as it is, and an empty list clears it. Rumoro normalizes each entry. Terms become lowercase, authors profile links or plain names, repositories owner/name, and subreddits lose the r/. New mentions follow the change within a minute. Stored mentions stay as they are.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `excludedTerms` | | Sets a new list. An empty list clears it. |
| `excludedAuthors` | | Sets a new list. An empty list clears it. |
| `excludedRepos` | | Sets a new list. An empty list clears it. |
| `subreddits` | | Applies to Reddit only. A list you leave out stays as it is. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The filters after the change |
| 400 | Could not parse an entry as a term, author, repository or subreddit |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Create a group
`POST /v1/groups`
Creates a keyword group. `name` must be unique in the workspace. `externalId` is optional and also unique. It is your own id, such as a client id, so you can find the group without storing ours. `context` is optional. It is a company description for this group, which the classifier reads instead of the workspace profile for the group's keywords. Then send the group's id as `groupId` when you create a keyword.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `name` | yes | The group's name, such as a client, campaign or product. Must be unique in the workspace. |
| `externalId` | | An id from your own system, such as a client id. Must be unique in the workspace. Find the group with GET /v1/groups?externalId=. |
| `context` | | Up to 4000 characters the classifier treats as "the company" for this group's keywords. It fully replaces the workspace profile, including its relevance guidelines and competitors. Describe the business, what it sells and to whom, what it is not, and any rule for this group, such as "ignore job posts". With one group per client, use the client's description. Null means the workspace profile is used. |
## Responses
| Status | Meaning |
| --- | --- |
| 201 | The group that was created |
| 401 | The API key is missing or not valid |
| 409 | This name or externalId is already used by a group (duplicate_group) |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Delete a group
`DELETE /v1/groups/{id}`
Deletes the group and all of its keywords, each as DELETE /v1/keywords/{id} would. Their mentions are deleted too, alert rules that named them are updated, and past charges stay in the usage record. Read the group first, since `stats.keywords` tells you how many keywords will be deleted. Only non-default groups can be deleted.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The group's id (grp_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 204 | The group and its keywords were removed |
| 401 | The API key is missing or not valid |
| 404 | This workspace has no group with this id |
| 409 | Deleting the default group is not allowed (default_group) |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get a group
`GET /v1/groups/{id}`
Returns one keyword group with its external id and keyword counts.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The group's id (grp_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The requested group |
| 401 | The API key is missing or not valid |
| 404 | This workspace has no group with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List groups
`GET /v1/groups`
Returns the workspace's keyword groups, the default group first and then the oldest. Groups organize keywords, for example by client, campaign or product. Every keyword is in one group, a group can hold a term once, and GET /v1/usage/breakdown?by=group shows each group's cost. `externalId` looks a group up by your own id.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `externalId` | query | Filters to the one group whose externalId matches exactly. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The workspace's groups |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Update a group
`PATCH /v1/groups/{id}`
Changes a group's name, your `externalId` for it or its company description in `context`. Null clears externalId. Null clears context too, so the workspace profile applies again. New mentions use the new context right away, and older ones are not scored again. You can rename the default group, but it takes no description, because it is the workspace itself and uses the company profile.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The group's id (grp_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `name` | | The group's name, such as a client, campaign or product. Must be unique in the workspace. |
| `externalId` | | Sets a new external id. Null removes it. |
| `context` | | Sets a new company description for the group. Null removes it, and the workspace profile applies again. New mentions use it right away, and older ones are not scored again. The default group cannot have one (400 default_group_context), because it is the workspace itself and uses the profile. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The group after the change |
| 400 | The default group cannot have a company description (default_group_context) |
| 401 | The API key is missing or not valid |
| 404 | This workspace has no group with this id |
| 409 | This name or externalId is already used by a group (duplicate_group) |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Invite a member
`POST /v1/members/invitations`
Invites an address by email as admin or member. Invitations expire after 48 hours. Repeating it for a pending address returns that invitation with 200 and sends nothing. Existing members get 409 already_member. Needs a signed-in owner or admin (dashboard session or MCP sign-in token). API keys get 403.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `email` | yes | Address that receives the join link. |
| `role` | | The role they get when they join. Ownership can only be given in the dashboard. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The pending invitation for this address |
| 201 | The invitation that was emailed |
| 401 | The API key is missing or not valid |
| 403 | An API key was used, or the signed-in user is not an owner or admin |
| 409 | This address belongs to a member already (already_member) |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List pending invitations
`GET /v1/members/invitations`
Returns invitations that have not been accepted, declined or expired yet. Once accepted, the person shows up in GET /v1/members.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The pending invitations |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List members
`GET /v1/members`
Returns all workspace members, owners first. Use `userId` for a mention's assigneeId and a person's ownerId.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The workspace's members |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Remove a member
`DELETE /v1/members/{id}`
Needs a signed-in owner or admin. Admins can't remove owners, and the last owner can't be removed at all (409 last_owner). Access ends within a minute, at the next dashboard request or when the OAuth token's short cache expires. The member's mentions, notes and outreach records remain.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The membership's id (mem_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 204 | Done |
| 401 | The API key is missing or not valid |
| 403 | Needs a signed-in owner or admin, not an API key. Only an owner can remove an owner |
| 404 | No member with this id |
| 409 | The workspace needs at least one owner (last_owner) |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Revoke an invitation
`DELETE /v1/members/invitations/{id}`
Cancels the invitation, and its email link stops working right away. Needs a signed-in owner or admin. An API key gets 403.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The invitation's id (inv_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 204 | The key no longer works |
| 401 | The API key is missing or not valid |
| 403 | An API key was used, or the signed-in user is not an owner or admin |
| 404 | No pending invitation with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Delete outreach
`DELETE /v1/people/{id}/activities/{activityId}`
Deletes an activity that was logged by mistake. The person keeps the same owner and stage.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The person's id (aut_...). |
| `activityId` | path, required | The activity's id (act_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 204 | Done |
| 401 | The API key is missing or not valid |
| 404 | This person has no activity with this id in your workspace |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Export people as CSV
`GET /v1/people/export.csv`
Returns as CSV the people GET /v1/people lists for the same filters, segmentId included. Each row is one person with handle, followers, email, website, company, location and tags, then outreach stage, owner and last contacted. The file holds at most 5,000 people. Each workspace can export 6 times a minute, and a 429 includes Retry-After.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `platform` | query | Only people with a profile on this platform. |
| `q` | query | Searches the display name, handle and profile link, ignoring case. |
| `handle` | query | Looks up a person by one of their accounts, given as a handle (@sam, u/sam, sam) or a profile or post link (https://x.com/sam). The match is exact but ignores case, and covers merged accounts. Add platform to pick one platform. A link already tells which platform it is. |
| `tag` | query | Keeps people with this tag, matched exactly and by case. |
| `muted` | query | true keeps muted people and false people who are not muted. Leave it out for everyone. |
| `since` | query | A time in ISO 8601 or epoch ms. Keeps people first matched then or later. |
| `segmentId` | query | Adds a saved segment's filter to all other filters here. An unknown id returns 404. |
| `platforms` | query | Keeps people who have an account on one of these platforms. Repeat the parameter or separate values with commas. |
| `tags` | query | Keeps people with one of these tags. Repeat the parameter or separate values with commas. |
| `minFollowers` | query | This many followers or more. People with an unknown count are left out. |
| `maxFollowers` | query | Keeps people with this many followers or fewer. |
| `minMentions` | query | Minimum number of matched mentions. |
| `minNegative` | query | Minimum number of negative mentions. |
| `intents` | query | Has a mention tagged with one of these intents. |
| `notPlatforms` | query | Leaves out people with an account on these platforms. Repeat the parameter or separate values with commas. |
| `notTags` | query | Leaves out people with one of these tags. Repeat the parameter or separate values with commas. |
| `notIntents` | query | Leaves out people whose mentions have one of these intents. Repeat the parameter or separate values with commas. |
| `keywordKinds` | query | Only people with a mention of these keyword kinds. |
| `neverKeywordKinds` | query | Excludes people who mentioned these keyword kinds. |
| `newSinceDays` | query | People first seen in the past this many days. |
| `linkHosts` | query | Keeps people with a mention that links to one of these hosts or its subdomains. Repeat the parameter or separate values with commas. |
| `stages` | query | Keeps people at one of these outreach stages. Repeat the parameter or separate values with commas. |
| `automated` | query | true keeps people whose matched posts are mostly machine-made, such as bots. false keeps the others. Leave it out for everyone. |
| `ownerIds` | query | Keeps people owned by one of these members, by user id. Use `none` for people without an owner. Repeat the parameter or separate values with commas. |
| `sort` | query | mentions puts the most matches first. recent puts the most recently seen first. reach puts the most followers first, with unknown counts at the end. new puts people seen for the first time most recently first. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | A UTF-8 CSV file |
| 400 | The query is not valid |
| 401 | The API key is missing or not valid |
| 404 | Your workspace has no segment with this id |
| 429 | Over 6 exports in this minute. Wait the Retry-After seconds and try again |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get a person
`GET /v1/people/{id}`
Fetches a person, with your workspace's own data on them. The id of a merged account returns the person it was merged into.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The person's id (aut_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The requested person |
| 401 | The API key is missing or not valid |
| 404 | Your workspace has no mention by this person |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List people
`GET /v1/people`
Returns the authors of your mentions, one row per person. Each row has their accounts, reach, public profile, stats for this workspace, your notes and tags, and your outreach status. You can filter by platform, tag, follower range, mention counts, intents, keyword kinds they did or did not mention, outreach stage, owner, automated (bots whose matched posts are mostly machine-made) or a saved segment. Pages use offset and include a total.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `platform` | query | Only people with a profile on this platform. |
| `q` | query | Searches the display name, handle and profile link, ignoring case. |
| `handle` | query | Looks up a person by one of their accounts, given as a handle (@sam, u/sam, sam) or a profile or post link (https://x.com/sam). The match is exact but ignores case, and covers merged accounts. Add platform to pick one platform. A link already tells which platform it is. |
| `tag` | query | Keeps people with this tag, matched exactly and by case. |
| `muted` | query | true keeps muted people and false people who are not muted. Leave it out for everyone. |
| `since` | query | A time in ISO 8601 or epoch ms. Keeps people first matched then or later. |
| `segmentId` | query | Adds a saved segment's filter to all other filters here. An unknown id returns 404. |
| `platforms` | query | Keeps people who have an account on one of these platforms. Repeat the parameter or separate values with commas. |
| `tags` | query | Keeps people with one of these tags. Repeat the parameter or separate values with commas. |
| `minFollowers` | query | This many followers or more. People with an unknown count are left out. |
| `maxFollowers` | query | Keeps people with this many followers or fewer. |
| `minMentions` | query | Minimum number of matched mentions. |
| `minNegative` | query | Minimum number of negative mentions. |
| `intents` | query | Has a mention tagged with one of these intents. |
| `notPlatforms` | query | Leaves out people with an account on these platforms. Repeat the parameter or separate values with commas. |
| `notTags` | query | Leaves out people with one of these tags. Repeat the parameter or separate values with commas. |
| `notIntents` | query | Leaves out people whose mentions have one of these intents. Repeat the parameter or separate values with commas. |
| `keywordKinds` | query | Only people with a mention of these keyword kinds. |
| `neverKeywordKinds` | query | Excludes people who mentioned these keyword kinds. |
| `newSinceDays` | query | People first seen in the past this many days. |
| `linkHosts` | query | Keeps people with a mention that links to one of these hosts or its subdomains. Repeat the parameter or separate values with commas. |
| `stages` | query | Keeps people at one of these outreach stages. Repeat the parameter or separate values with commas. |
| `automated` | query | true keeps people whose matched posts are mostly machine-made, such as bots. false keeps the others. Leave it out for everyone. |
| `ownerIds` | query | Keeps people owned by one of these members, by user id. Use `none` for people without an owner. Repeat the parameter or separate values with commas. |
| `sort` | query | mentions puts the most matches first. recent puts the most recently seen first. reach puts the most followers first, with unknown counts at the end. new puts people seen for the first time most recently first. |
| `limit` | query | How many to return, from 1 to 100. |
| `offset` | query | People to skip. Offset paging is for grouped lists of hundreds, not streams. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | A page of people |
| 400 | The query is not valid |
| 401 | The API key is missing or not valid |
| 404 | Your workspace has no segment with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List outreach activities
`GET /v1/people/{id}/activities`
Returns up to 200 logged contacts with this person over all their accounts, latest first. Each shows who reached out, the channel, the time and a short note. Check it before you reach out, so two teammates don't contact the same person unaware.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The person's id (aut_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The person's logged activities |
| 401 | The API key is missing or not valid |
| 404 | Your workspace has no mention by this person |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Log outreach
`POST /v1/people/{id}/activities`
Logs a contact with this person, such as an email, a direct message or a call. A not_contacted person moves to contacted. A person with no owner gets the contacting member as owner. A later stage or an existing owner is left alone. `memberId` falls back to the signed-in member. An API-key request without memberId logs an activity with no member, so no owner is set.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The person's id (aut_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `channel` | yes | How the person was contacted. |
| `note` | | A short summary of what was sent or said. |
| `occurredAt` | | Defaults to now. Otherwise send the contact time as ISO 8601 or epoch ms. |
| `memberId` | | The user id of the member who reached out. Defaults to the signed-in member. With an API key and no value, the activity has no member. |
## Responses
| Status | Meaning |
| --- | --- |
| 201 | The new activity |
| 400 | The body is not valid, or memberId is not in the workspace |
| 401 | The API key is missing or not valid |
| 404 | Your workspace has no mention by this person |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Merge people
`POST /v1/people/{id}/merge`
Marks this account and another person as the same individual, in your workspace only. The person in `into` receives the mentions, tags, notes and outreach activities, and keeps its owner and stage, taking the other's where it has none.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The person's id (aut_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `into` | yes | The id of the person to merge this account into. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The person who now has this account |
| 401 | The API key is missing or not valid |
| 404 | Your workspace does not know one of the two |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Split a person
`POST /v1/people/{id}/split`
Reverses a merge, so the account is a separate person again.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The person's id (aut_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The account as a separate person |
| 401 | The API key is missing or not valid |
| 404 | This account is not merged into another person |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Update a person
`PATCH /v1/people/{id}`
Changes your workspace's tags, notes and mute for the person, and the outreach owner and stage. The owner must be a workspace member, and null removes it. Muting hides their posts from your feed and all channels. Collection and billing do not change.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The person's id (aut_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `tags` | | Sets the full new list. |
| `notes` | | |
| `muted` | | |
| `ownerId` | | The user id of the member responsible for this contact. Null removes it. |
| `stage` | | The outreach stage your team has reached with this person. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The person after the change |
| 400 | The body is not valid, or the owner is not in the workspace |
| 401 | The API key is missing or not valid |
| 404 | Your workspace has no mention by this person |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Create a segment
`POST /v1/segments`
Saves a people filter under a name. Names are unique in the workspace (409). The response says how many people match now.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `name` | yes | |
| `description` | | |
| `filter` | | |
## Responses
| Status | Meaning |
| --- | --- |
| 201 | The new segment and how many people are in it |
| 400 | The body is not valid |
| 401 | The API key is missing or not valid |
| 409 | This name is already used by a segment |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Delete a segment
`DELETE /v1/segments/{id}`
Deletes the segment. The people in it are not changed.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The segment's id (seg_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 204 | Done |
| 401 | The API key is missing or not valid |
| 404 | Your workspace has no segment with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get a segment
`GET /v1/segments/{id}`
Returns one segment with its filter and how many people match it now.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The segment's id (seg_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The segment and how many people are in it |
| 401 | The API key is missing or not valid |
| 404 | Your workspace has no segment with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List segments
`GET /v1/segments`
Returns your saved segments with the current number of people in each, plus presets you can save as a start. Segments are counted on each read and never stored as lists. To see who is in one, send its id to GET /v1/people.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | All segments and the presets |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Update a segment
`PATCH /v1/segments/{id}`
Changes a segment's name, description or filter. A new filter replaces the old one completely.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The segment's id (seg_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `name` | | |
| `description` | | |
| `filter` | | Sets the full new filter. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The segment after the change |
| 401 | The API key is missing or not valid |
| 404 | Your workspace has no segment with this id |
| 409 | This name is already used by a segment |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get the usage breakdown
`GET /v1/usage/breakdown`
Returns what the workspace used and was charged over a period, in US cents at list price. Each call groups rows by one `by` value, day, platform or keyword, and always includes the totals. `range` reads the past UTC days up to today (30d by default). `month` reads one calendar month (YYYY-MM), which is what you match against a bill or a per-client margin. Keyword-days come from the daily run and mention charges from billed matches. So a deleted keyword keeps its charges in the keyword rows (`keyword.removed`), while its mention counts show 0. Each keyword carries the same numbers for the current month as `stats.cost`. `totals.ledgerDebitCents` is what the balance has been debited so far for the period's days. Mentions settle the morning after, so a period ending today is below `totals.totalCents` by the unsettled ones, and a finished month differs only by cumulative rounding. Rows come in pages (`limit`, `offset`, `total`). A workspace can call this 30 times a minute across all its keys and tokens.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `by` | query | How to group the rows. day gives one row per UTC day, then platform, keyword (the default, used to work out margins), or group (the cost of a client or campaign). |
| `range` | query | How many UTC days back from today to read. One of 7d, 30d or 90d, 30d by default. Ignored when you send `month`. |
| `month` | query | A calendar month (YYYY-MM, UTC) to read instead of a range. It covers the month's first to last day, or up to today for the current month. A future month returns 400. |
| `limit` | query | How many rows to return, from 1 to 500, 100 by default. Only by=keyword can need more than one page, since a period has at most 90 days and there are only a dozen platforms. |
| `offset` | query | How many rows to skip. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The period's totals and a page of rows |
| 400 | Invalid by, range, month or limit. A future month is invalid too |
| 401 | The API key is missing or not valid |
| 429 | The workspace read the breakdown over 30 times in this minute |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get usage and balance
`GET /v1/usage`
Shows whether tracking is stopped or the balance is low. Also returns the prepaid balance (ledger total, pending mention charges, and the effective balance that decides pausing), daily spend and days left, running and paused keywords, and matches today and over 30 days. Pricing is $0.008 per matched mention, relevant or not, and $5 a month per active keyword, charged daily.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | A summary of usage |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Create a view
`POST /v1/views`
Saves a named mention filter. It uses the same fields as GET /v1/mentions, where a list matches any value, a `not` list matches none, and all conditions are joined by AND. `anyOf` adds groups of such conditions, at least one of which must match. An empty filter shows all mentions. Nothing is stored ahead of time, so a view shows whatever matches when you read it.
## Body
| Field | Required | Description |
| --- | --- | --- |
| `name` | yes | Must be unique in the workspace, ignoring case. |
| `description` | | The view's purpose, shown below its name. |
| `filter` | | The view's filter. Empty means all mentions. |
## Responses
| Status | Meaning |
| --- | --- |
| 201 | The new view |
| 400 | The filter is not valid, for example an empty anyOf group |
| 401 | The API key is missing or not valid |
| 409 | This name is already used by a view (duplicate_view) |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Delete a view
`DELETE /v1/views/{id}`
Deletes the view. Its mentions are unchanged.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The view's id (vw_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 204 | Done |
| 401 | The API key is missing or not valid |
| 404 | This workspace has no view with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Get a view
`GET /v1/views/{id}`
Returns one saved view and its filter. Pass its id as `viewId` to list or export the mentions it shows.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The view's id (vw_...). |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The requested view |
| 401 | The API key is missing or not valid |
| 404 | This workspace has no view with this id |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# List views
`GET /v1/views`
Lists the saved views in creation order. A view stores a mention filter under a name. GET /v1/mentions and the exports take its id as `viewId` and return what the view shows.
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The workspace's views |
| 401 | The API key is missing or not valid |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).
# Update a view
`PATCH /v1/views/{id}`
Changes a view's name, description or filter. `filter` sets the full new filter.
## Parameters
| Name | In | Description |
| --- | --- | --- |
| `id` | path, required | The view's id (vw_...). |
## Body
| Field | Required | Description |
| --- | --- | --- |
| `name` | | |
| `description` | | |
| `filter` | | Sets the full new filter. |
## Responses
| Status | Meaning |
| --- | --- |
| 200 | The view after the change |
| 401 | The API key is missing or not valid |
| 404 | This workspace has no view with this id |
| 409 | This name is already used by a view (duplicate_view) |
The [OpenAPI document](https://api.rumoro.dev/v1/openapi.json) has every schema. Rules shared by all endpoints are in [Conventions](https://docs.rumoro.dev/conventions.mdx).