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.
| Octolens | Rumoro | |
|---|---|---|
| Base URL | https://app.octolens.com | https://api.rumoro.dev/compat/octolens |
| Key | Bearer <octolens key> | 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 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
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 twitterwhoami shows your Rumoro workspace, and you don't need octolens login. Tested with octolens 0.1.8.
Your own code
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.
// 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');OCTOLENS_API_KEY=... RUMORO_API_KEY=ref_... node copy-from-octolens.mjsCopied 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. Create them under Alerts or with POST /v1/alerts. 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), so add the check to your handler when you move it.
Step 4. Switch over
- Run step 2, keep both tools running for a day, and compare the results of
POST /api/v2/mentions. - Point your code and the CLI at Rumoro.
- Set up alerts to replace your notifications, and test each one.
- 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, 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 and alerts.