OverviewPlatformsAPI Reference

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.

OctolensRumoro
Base URLhttps://app.octolens.comhttps://api.rumoro.dev/compat/octolens
KeyBearer <octolens key>Bearer ref_...
Paths, JSON, errors/api/v2/...The same
Rate limit500 requests an hour600 a minute per workspace
PricePlans 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 twitter

whoami 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.

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');
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. Create them under Alerts or with POST /v1/alerts. Alerts go to Slack, Telegram, email or a webhook.

OctolensRumoro alert
frequency: hourly or hourlyAtTopOfHour, deliveryMode: batchmode: hourly, sent five minutes past each UTC hour and skipped when empty. Not available by email.
frequency: daily or weekly, with time, timezone, dayOfWeekmode: daily or weekly, with schedule.hour, minute, timezone, weekday
deliveryMode: individual, and every webhookmode: 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

  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

TopicHow it works in Rumoro
IdsNumbers, 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.
RowsOne row per matched keyword, where Octolens combines them into one.
Titles and imagestitle and imageUrl when the platform has them
ListsOnly relevant mentions unless includeAll: true. Muted people and snoozed mentions are left out. At most 50 keyword ids. Pages of 20, up to 100.
RelevancerelevanceScore 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.
TagsRumoro's intents under Octolens' names (product_question, user_feedback and so on), plus own_brand_mention, competitor_mention and ai_generated
Platformstwitter 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 matchingWhole phrases only (symbolSensitive has no effect). * wildcards work only at the start or end. isSubReddit returns 501, so use positiveSubreddits.
FiltersAND and OR groups become anyOf, up to 10 alternatives. Content returns 501, so use search. TwitterFollowerCount accepts >=, <= and =.
Global filtersadd skips duplicates and lists what it refused under dropped. Emptying a list needs allowEmpty: true.
FeedsSaved as views, without the icon. Destinations return 501. A view this API can't represent is read-only here (409 FEED_FILTER_WRITE_CONFLICT).
ExportsUp to 5,000 mentions. author exports one person's mentions. Read-only keys can export.
Keywords2 to 80 characters. Sending an existing term returns that keyword, and with allowDuplicate: true it returns 409 ITEM_EXISTS.
CompanyclassificationGuidelines returns 501, so use relevanceGuidelines.
AnalyticsUTC days, 30 by default, high and medium relevance unless you set relevance=0,1,2. Tag or sentiment filters return 501.
PathsPaths outside /api/v2, or with a trailing slash, return 404. Unknown /api/v2 paths return 501.
Usagementions.limit is how many mentions your balance still covers this month.
Rate limitShared 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.

Was this page helpful?

On this page