---
title: "Duplicate a keyword"
method: POST
path: "/v1/keywords/{id}/duplicate"
tags: ["Keywords"]
---

# Duplicate a keyword

`POST /v1/keywords/{id}/duplicate`

## Request

- Base URL: `https://api.rumoro.dev`
- URL: `https://api.rumoro.dev/v1/keywords/{id}/duplicate`
- Auth: HTTP bearer

Creates a new keyword from this one's settings under a new `term`, or the same term in another group: its kind, platforms, context, every matching rule, its monthly mention cap and its comments setting. Review pages and feeds are copied only when named in `include`. The copy is a new keyword: $5 a month, and the newest posts of the last 30 days come in at once, as for any new keyword.

## Path parameters

- `id` string, required: Keyword id (kw_...).

## Request body

- object
  - `term` string, required: The copy's term. The original's term is allowed only in another group (groupId), since a group tracks a term once.
  - `groupId` string: The copy's group (grp_...). Omitted, it joins the original's group.
  - `include` string[]: Also copy these: reviewSources (its review pages) and feeds (its RSS or Atom feeds). Off by default, because two keywords on one page or feed collect, and bill, every review or item twice.

## Response `201`

The new keyword

- Keyword
  - `id` string, required: The keyword's id (kw_...).
  - `term` string, required
  - `kind` 'brand' | 'competitor' | 'topic', required
  - `muted` boolean, required: True when the keyword is neither polled nor matched. You, the wallet (pausedForBalance) or the noise brake (pausedForNoise) can mute it. Reaching the mention cap does not mute it (pausedForCap).
  - `pausedForBalance` boolean, required: Muted because the balance ran out. A top-up resumes it. Unmuting it yourself also needs balance.
  - `pausedForNoise` boolean, required: Set when the noise brake muted the keyword. That happens on welcome credit once 20 or more matches are scored and under 30% are relevant. Unmuting it, or changing its required or excluded terms, platforms or context, resumes it when the balance covers another day. A top-up alone does not.
  - `pausedForCap` boolean, required: Hit its monthly mention cap. Matching resumes on the 1st (UTC) or when you raise the cap. The keyword isn't muted and is still charged daily.
  - `cap` object, nullable, required: Monthly limit on matched mentions. Null means none.
    - `mentions` integer, required: How many matched mentions the keyword may collect per calendar month (UTC). Every match counts, relevant or not, including a new keyword's look-back, since every match is billed.
    - `welcome` boolean, required: Rumoro sets this while the workspace uses its welcome credit. It limits each keyword to 200 mentions a month until the first top-up.
    - `own` integer, nullable, required: The cap you chose. While welcome is true it is the cap restored at the first top-up (null for none), and sending `mentions: 200` changes nothing. Otherwise it equals mentions.
  - `comments` object, required: Comments under this keyword's mentions. When enabled, the thread of every new mention scored relevant is read from 30 minutes after the post, several times while its platform's conversations last (a day on Reddit, Hacker News and Bluesky, a week on GitHub, Stack Overflow and DEV, a month on YouTube): new comments only, newest first, at most maxPerPost per mention, and comments of fewer than three words are dropped. List them with GET /v1/mentions/{id}/comments.
    - `enabled` boolean, required: Read the comments under this keyword's relevant mentions, from 30 minutes after each post, while its platform's conversations last. Each comment delivered costs $0.008 on the comments line of the bill (comments of fewer than three words are dropped, never billed). Off by default.
    - `maxPerPost` integer, required: How many of a thread's newest comments you receive, 20 by default and 100 at most: the ceiling on what one mention's comments can cost. A comment that names the keyword is a mention too, but only among these.
  - `group` GroupRef, required: The keyword's group.
    - `id` string, required: The group's id (grp_...).
    - `name` string, required: Name of the group.
    - `externalId` string, nullable, required: An id from your own system, or null.
    - `isDefault` boolean, required: True for the workspace's default group. New keywords go there when you don't name a group.
  - `platforms` string[], nullable, required: The platforms searched for the term. Null means all of them. An empty list means none, and the keyword only collects reviews.
  - `feeds` KeywordFeed[], required: The RSS or Atom feeds this keyword reads, in the order given.
    - `url` string, required: The feed itself, after redirects: the address that is read.
    - `title` string, nullable, required: The feed's own title, when it has one.
    - `connectedAt` string, required: When this keyword started reading the feed. Nothing published before it is a live mention.
  - `reviewSources` ReviewSource[], required: The review pages the keyword reads, such as App Store and Google Play apps, Trustpilot pages and Google Maps places. Empty when there are none.
    - `platform` 'appstore' | 'googleplay' | 'trustpilot' | 'googlemaps', required: Where the reviews come from. appstore is the Apple App Store, googleplay is Google Play, trustpilot is a company's Trustpilot page and googlemaps is the Google reviews of a place.
    - `id` string, required: The platform's own id. An App Store app id, a Google Play package name, a Trustpilot domain, or a Google Place ID or cid.
    - `url` string, required: URL of the review page, such as an app's store listing, a company's Trustpilot page or a place on Google Maps.
    - `countries` string[], required: Two-letter storefront codes, lowercase. Empty for Trustpilot and Google Maps.
    - `language` string, nullable, required: The review language on Google Play. Null on other platforms.
    - `connectedAt` string, required: When the keyword began collecting reviews from this page.
  - `context` string, nullable, required: 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 "Basil is our deploy tool, not the herb." Null clears it.
  - `matching` object, required: Rules a post must pass before it is stored as a mention. A post they reject is never billed.
    - `requiredTerms` string[], required: Terms the post must contain as well. requiredMode says whether one or all of them must appear. An empty list sets no requirement.
    - `requiredMode` 'any' | 'all', required: any needs one of the required terms in the post. all needs every one.
    - `excludedTerms` string[], required: Posts that contain one of these terms are dropped. Put `*` at the start or end of a term as a wildcard, so deploy* matches deployment and *bot matches nightlybot.
    - `excludedAuthors` string[], required: Authors whose posts this keyword ignores. Each entry is a profile or post link, @handle, u/name, Bluesky DID or display name. Entries are normalized the same way as an alert's muted authors.
    - `caseSensitive` boolean, required: When true, the term only matches in the exact case you typed, so RAG never matches rag. Defaults to false.
  - `stats` object, required: Counted from this workspace's matches only.
    - `mentions` integer, required: All matches so far, relevant or not. Billing counts this number.
    - `relevant` integer, required: Matches whose relevance score reached the threshold.
    - `last7d` integer, required: Matches with a post date in the past 7 days.
    - `thisMonth` integer, required: Matches recorded in the current calendar month (UTC). The cap is checked against this count.
    - `lastMentionAt` string, nullable, required: Time of the latest matched post. Null before the first match.
    - `feedback` object, required: Your team's relevance marks, set with PATCH /v1/mentions/{id}.
      - `relevant` integer, required: Marked relevant by your team.
      - `notRelevant` integer, required: Mentions someone on your team marked not relevant. This is noise the classifier missed.
    - `noise` object, required: Relevance of the scored matches from the past 14 days. A keyword you narrow today drops the flag within two weeks.
      - `scored` integer, required: How many of the last 14 days' matches are scored.
      - `relevant` integer, required: How many of them were scored relevant.
      - `noisy` boolean, required: True when the past 14 days have 20 or more scored matches and fewer than 30% are relevant. Narrow the keyword with required terms, excluded terms or context, because every match is billed, relevant or not.
    - `health` 'healthy' | 'noisy' | 'quiet' | 'capped' | 'paused' | 'new', required: Health over the 14 days `noise` covers, by the same rule as GET /v1/keywords/{id}/health. The first that applies wins. paused (muted), capped (at its cap), noisy (20+ scored matches, under 30% relevant), new (under 7 days old, or changed in the last 7 days with under 20 scored since), quiet (7+ days, nothing relevant), otherwise healthy. After a change to matching rules, platforms or context, or an unmute, within the 14 days, only later matches count, while `noise` keeps all 14 days. The endpoint defaults to 30 days, so the two can differ, and it explains why and what to change.
    - `cost` object, required: List-price cost this calendar month (UTC), equal to this keyword's row in GET /v1/usage/breakdown?month=<this month>. The ledger settles daily and can differ only by cumulative rounding.
      - `keywordDays` integer, required: Charged days this month. A day is charged when the keyword is unmuted at the daily run, so a new keyword shows 0 until tomorrow.
      - `keywordCents` integer, required: keywordDays at 500/30 cents each, which is $5 a month. Only the total is rounded.
      - `billableMentions` integer, required: Matches billed this month. A match counts when it is scored, which is the time the ledger settles by. So this can lag behind thisMonth while matches are still being scored, and a match that failed to score never counts.
      - `mentionCents` integer, required: Those matches priced at $0.008 each, with the total rounded once.
      - `billableComments` integer, required: Comments billed on the comments line: comments delivered under mentions and matched replies, each once per workspace.
      - `commentCents` integer, required: Those comments at $0.008 each, rounded once on the total.
      - `totalCents` integer, required: keywordCents plus mentionCents plus commentCents.
  - `polling` object[], required: Polling status for each platform that is polled on a schedule. Live feeds such as Bluesky are not listed.
    - `platform` 'bluesky' | 'hackernews' | 'github' | 'stackoverflow' | 'devto' | 'reddit' | 'x' | 'youtube' | 'news' | 'linkedin' | 'tiktok' | 'instagram' | 'appstore' | 'googleplay' | 'trustpilot' | 'googlemaps' | 'rss', required: The platform. Review platforms are appstore, googleplay, trustpilot and googlemaps (a place's Google reviews).
    - `lastPolledAt` string, nullable, required: When the term was last polled on this platform. Null before the first poll.
    - `emptyPolls` integer, required: How many polls in a row found nothing new. The higher it gets, the less often the term is polled.
  - `createdAt` string, required: UTC time in ISO 8601.

## Other responses

- `401`: Missing or invalid API key
- `402`: Not enough balance for one more keyword-day (insufficient_balance), or the 500-keyword ceiling is reached (keyword_limit_reached)
- `404`: Keyword not found
- `409`: The group already tracks this term

---

[API](https://skmtc.dev/rumoro/apis/rumoro-api.md) · [All operations](https://skmtc.dev/rumoro/apis/rumoro-api/llms.txt) · [OpenAPI document](https://skmtc.dev/rumoro/apis/rumoro-api/revisions/9ef63ea894d1?raw)
