Pick, dismiss or add a competitor
/sites/{siteId}/competitors- Scope
actions:apply- MCP tool
update_competitors
Pick, dismiss or add a competitor pick: a suggestion becomes a picked competitor; dismiss: a suggestion or picked competitor is left out from now on; add: any domain becomes a picked competitor. Picking or adding fetches its keyword gaps (a paid lookup inside the spending limits). Recorded in Activity as the key owner. Answers with the new competitor set.
Parameters
| Name | In | Details |
|---|---|---|
siteIdRequired | path | stringWebsite id, from GET /sites. |
Request body
| Field | Details |
|---|---|
action | pick | dismiss | add · required; What to do |
domain | string · required; The competitor domain, like example.com (scheme, www. and path are stripped) |
{
"action": "add",
"domain": "cyclehub.example"
}Response
200 · application/json
| Field | Details |
|---|---|
action | pick | dismiss | add · required; What was done |
domain | string · required; The domain, cleaned |
competitors | list of values · required; Picked first, then suggestions the AI confirmed, then ones not reviewed yet |
competitors[].domain | string |
competitors[].kind | string |
competitors[].score | number |
competitors[].review | object |
competitors[].review.status | string |
competitors[].review.sameKind | |
competitors[].review.kind | |
competitors[].review.reason | |
competitors[].drivesGaps | true or false |
competitors[].sharedKeywords | number |
competitors[].brandKeywords | number |
competitors[].theirKeywords | number |
competitors[].ourKeywords | number |
competitors[].aboveUs | number |
competitors[].domainRank | number |
competitors[].category | string |
competitors[].visibility | list of numbers |
competitors[].visibilityMonths | list of strings |
competitors[].dataUpdatedAt | string |
ourVisibility | list of number · required; The website's own estimated monthly search traffic, oldest first |
ourVisibilityMonths | list of string · required; Month (YYYY-MM) of each ourVisibility value |
driving | list of string · required; The competitors gaps, link gap and "best X" lists use: picked, else the three strongest confirmed suggestions |
hidden | integer · required; Suggestions left out: weak, or turned down by the AI |
note | string · required; How to read the numbers |
link | address (URL) · required; The Competitors screen in the app |
Notes
GET /sites/{siteId}/competitors lists the sites you compete with: the ones you picked first, then the ones MonoRanks suggests because they rank for the same searches. Suggestions stay suggestions until someone picks them.
| What | Details |
|---|---|
score |
(0 to 100) says how well a suggestion matches. Shared keywords are weighed against the size of both keyword lists, so a giant site that shares a few hundred of its millions of keywords scores low. Shared keywords explained by a brand name do not count (brandKeywords). |
review |
is the AI check of a suggestion against what your website is about: status (reviewed or not_reviewed), sameKind (is it the same kind of business or product) and a short reason. Suggestions the AI turns down are hidden; hidden counts them together with weak ones. Without AI, status is not_reviewed and the score decides. |
driving |
(and drivesGaps on each row) are the competitors that keyword gaps, the link gap and the “best” lists use: the ones you picked; if none are picked, the three strongest suggestions the AI confirmed; before any suggestion has been reviewed, the three strongest by score. |
POST /sites/{siteId}/competitors changes the set. action is pick (make a suggestion a picked competitor), dismiss (hide a suggestion or a picked competitor from then on) or add (add any domain as a picked competitor). Picking or adding looks up that competitor’s keyword gaps, a paid lookup inside your workspace’s data limit. The change is recorded in Activity as the key owner, and the reply is the updated set. Errors: bad_action, bad_domain (also for your own website, a reserved test address, or more than 10 picked competitors) and not_found when pick or dismiss names a domain that is not in the set.
Errors
| Status | Meaning |
|---|---|
400 | Bad request. Codes: bad_action, bad_domain. |
401 | Missing, unknown or revoked key. Codes: missing_key, invalid_key. |
403 | The key lacks the scope, covers another website, or its owner lost access. Codes: missing_scope, other_site, owner_left, no_owner, forbidden. |
404 | Not found. Codes: not_found. |
429 | Too many requests this hour for this key. Codes: rate_limited. |
Example request
curl -X POST \
-H "Authorization: Bearer mr_ws_…" \
-H "Content-Type: application/json" \
-d '{"action":"add","domain":"cyclehub.example"}' \
"https://app.monoranks.com/api/v1/sites/SITE_ID/competitors"const res = await fetch("https://app.monoranks.com/api/v1/sites/SITE_ID/competitors", {
method: "POST",
headers: { Authorization: "Bearer mr_ws_…", "Content-Type": "application/json" },
body: JSON.stringify({
"action": "add",
"domain": "cyclehub.example"
}),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const data = await res.json();import requests
res = requests.post(
"https://app.monoranks.com/api/v1/sites/SITE_ID/competitors",
headers={"Authorization": "Bearer mr_ws_…"},
json={
"action": "add",
"domain": "cyclehub.example",
},
)
res.raise_for_status()
data = res.json()<?php
$ch = curl_init("https://app.monoranks.com/api/v1/sites/SITE_ID/competitors");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["Authorization: Bearer mr_ws_…", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"action":"add","domain":"cyclehub.example"}',
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);import axios from "axios";
const { data } = await axios.post(
"https://app.monoranks.com/api/v1/sites/SITE_ID/competitors",
{
"action": "add",
"domain": "cyclehub.example"
},
{ headers: { Authorization: "Bearer mr_ws_…" } },
);Example response
{
"action": "add",
"domain": "cyclehub.example",
"competitors": [
{
"domain": "bikeworld.example",
"kind": "picked",
"score": 78,
"review": {
"status": "not_reviewed",
"sameKind": null,
"kind": null,
"reason": null
},
"drivesGaps": true,
"sharedKeywords": 412,
"brandKeywords": 6,
"theirKeywords": 5810,
"ourKeywords": 1240,
"aboveUs": 188,
"domainRank": 41,
"category": "Shop",
"visibility": [
32800,
35600,
37900,
41600
],
"visibilityMonths": [
"2026-06",
"2026-07",
"2026-08",
"2026-09"
],
"dataUpdatedAt": "2026-10-01T04:20:00.000Z"
},
{
"domain": "cyclehub.example",
"kind": "suggested",
"score": 64,
"review": {
"status": "reviewed",
"sameKind": true,
"kind": "Online bike shop",
"reason": "Sells bikes and parts to riders, like Acme Bikes."
},
"drivesGaps": true,
"sharedKeywords": 412,
"brandKeywords": 6,
"theirKeywords": 5810,
"ourKeywords": 1240,
"aboveUs": 188,
"domainRank": 41,
"category": "Shop",
"visibility": [
32800,
35600,
37900,
41600
],
"visibilityMonths": [
"2026-06",
"2026-07",
"2026-08",
"2026-09"
],
"dataUpdatedAt": "2026-10-01T04:20:00.000Z"
}
],
"ourVisibility": [
12400,
13900,
14800,
15700
],
"ourVisibilityMonths": [
"2026-06",
"2026-07",
"2026-08",
"2026-09"
],
"driving": [
"bikeworld.example"
],
"hidden": 3,
"note": "kind: picked (chosen by a person) or suggested. Suggestions stay suggestions until picked.",
"link": "https://app.monoranks.com/sites/3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6f4c13/competitors"
}{
"error": {
"code": "bad_action",
"message": "action must be one of pick, dismiss, add."
}
}{
"error": {
"code": "missing_key",
"message": "Send the key as Authorization: Bearer mr_site_…"
}
}{
"error": {
"code": "missing_scope",
"message": "This key does not have the scope this call needs."
}
}{
"error": {
"code": "not_found",
"message": "cyclehub.example is not in the competitor set. GET /competitors lists it; use action add for a new domain."
}
}{
"error": {
"code": "rate_limited",
"message": "More than 1200 requests this hour for this key."
}
}The MCP server does the same with the tool update_competitors.