v1Resources/SEO

SEO

Track local-search keyword rankings on a geographic grid, and competitor standing.

Returns tracked keywords, monthly rank trend, the ranking grid, and stat cards for one location. Pass keywordId to scope the grid to a single keyword, or month (YYYY-MM) to pin it to a specific month instead of each keyword's latest measured report.

Get a location's ranking overview

get/api/v1/seo/keywords
seo:read
Query parameters
locationIdstringrequired
The location to look up.
clientIdstringoptional
The location's client.
keywordIdstringoptional
Restrict the grid to one tracked keyword. Omit for the aggregated best-rank-per-cell grid across all active keywords.
monthstringoptional
Pin the grid and stats to this month (YYYY-MM) instead of each keyword's latest measured report.
Response
dataobjectoptional
Additional internal fields may be present but are not part of the stable contract — rely only on the fields documented here.
keywordsarray of objectoptional
Tracked keywords for this location, each with its current rank and monthly trend.
idstringoptional
Unique identifier for the tracked keyword.
keywordstringoptional
The keyword text.
avgRanknumberoptional
The keyword's latest measured average rank across the grid (lower is better; null if never measured or not ranking).
monthlyarray of objectoptional
This keyword's average rank for each month it has a report.
monthstringoptional
ranknumberoptional
monthsarray of objectoptional
Every month with at least one measured report, oldest first, with the mean average rank across keywords for that month.
keystringoptional
Month in YYYY-MM form.
labelstringoptional
Human-readable label for the month, e.g. "Aug 2026".
avgRanknumberoptional
Mean average rank across tracked keywords for this month, or null.
gridarray of objectoptional
The ranking grid: one entry per scraped point, merged across the in-scope keyword(s)/month.
idstringoptional
rowintegeroptional
Grid row index.
colintegeroptional
Grid column index.
latnumberoptional
Latitude of this grid point.
lngnumberoptional
Longitude of this grid point.
ranknumberoptional
Rank found at this point (lower is better); null if not measured, values above 20 mean "20+".
excludedbooleanoptional
Whether this point is excluded from the location's stat calculations.
waterbooleanoptional
Whether this point falls in water (sea, lake, wide river) and is only counted if the location has opted in to including water points.
statsobjectoptional
Aggregate rank stats across the active grid.
top3Pctnumberoptional
Percent of grid points ranking in the top 3, or null.
top10Pctnumberoptional
Percent of grid points ranking in the top 10, or null.
avgRanknumberoptional
Average rank across the grid, or null.
statCardsobjectoptional
Highlight cards summarizing the location's best/worst keyword and biggest month-over-month mover.
bestobjectoptional
The keyword with the best (lowest) current average rank, or null if none has ranked.
keywordstringoptional
avgRanknumberoptional
worstobjectoptional
The keyword with the worst current average rank, or null.
keywordstringoptional
avgRanknumberoptional
gainerobjectoptional
The keyword whose rank improved the most from its first to its latest month, or null.
keywordstringoptional
fromnumberoptional
tonumberoptional
dropperobjectoptional
The keyword whose rank declined the most from its first to its latest month, or null.
keywordstringoptional
fromnumberoptional
tonumberoptional
notRankingCountnumberoptional
Grid points that were measured successfully but rank outside the top 20, or null.
noDataCountnumberoptional
Grid points that could not be measured at all (the scrape failed), distinct from not ranking, or null.
centerobjectoptional
The map center point for this location's grid (its business location), or null if not yet configured.
latnumberoptional
lngnumberoptional
includeWaterPointsbooleanoptional
Whether this location has opted in to counting grid points that fall in water.
Errors
400The request is missing a required parameter or is otherwise malformed.
401Missing, invalid, expired, or revoked API key.
403The key lacks the required scope, or isn't authorized for this client/location.
429Too many requests. Retry after the number of seconds in the Retry-After header.
get/api/v1/seo/keywords
Your API key
locationId *
clientId
keywordId
month
{
  "data": {
    "keywords": [
      {
        "id": "cm_kw_abc123",
        "keyword": "dentist near me",
        "avgRank": 4.2,
        "monthly": [
          {
            "month": "2026-01",
            "rank": 5.1
          },
          {
            "month": "2026-02",
            "rank": 4.2
          }
        ]
      }
    ],
    "months": [
      {
        "key": "2026-01",
        "label": "Jan 2026",
        "avgRank": 5.1
      },
      {
        "key": "2026-02",
        "label": "Feb 2026",
        "avgRank": 4.2
      }
    ],
    "grid": [
      {
        "id": "cm_pt_1",
        "row": 0,
        "col": 0,
        "lat": 30.2682,
        "lng": -97.7441,
        "rank": 3,
        "excluded": false,
        "water": false
      }
    ],
    "stats": {
      "top3Pct": 44,
      "top10Pct": 78,
      "avgRank": 4.2
    },
    "statCards": {
      "best": {
        "keyword": "dentist near me",
        "avgRank": 4.2
      },
      "worst": {
        "keyword": "emergency dentist austin",
        "avgRank": 9.6
      },
      "gainer": {
        "keyword": "dentist near me",
        "from": 5.1,
        "to": 4.2
      },
      "dropper": {
        "keyword": "family dentist",
        "from": 3.8,
        "to": 5.2
      },
      "notRankingCount": 1,
      "noDataCount": 0
    },
    "center": {
      "lat": 30.2672,
      "lng": -97.7431
    },
    "includeWaterPoints": false
  }
}
v1Resources/SEO/postAdd tracked keywords to a location

Starts tracking one or more keywords on a location's local-search ranking grid, configuring the grid size and radius the first time the location is set up. Keywords are deduped case-insensitively and capped at 25 per location. Triggers a background ranking scan for each newly added keyword — poll GET /api/v1/seo/keywords afterward to see results land.

Add tracked keywords to a location

post/api/v1/seo/keywords
seo:write
Request body
locationIdstringrequired
The location to track keywords for.
clientIdstringoptional
The location's client.
keywordsarray of stringrequired
Keywords to start tracking, up to 25 (deduped case-insensitively; the first spelling submitted wins).
gridSizeinteger (3 | 5 | 7)optional
Grid size (3, 5, or 7 points per side). Only applied the first time this location's grid is configured — later calls that omit it, or that target an already-configured location, leave the stored grid size unchanged.
distanceKmnumberoptional
Distance in kilometers between adjacent grid points. Same first-time-only rule as gridSize; defaults to 1.5 km when the location has no grid configured yet.
excludedarray of objectoptional
Grid points to exclude from measurement — e.g. a lake, a rival's territory. Points are still scraped either way; this only controls whether they count toward stats.
rowintegeroptional
Grid row index, 0-indexed.
colintegeroptional
Grid column index, 0-indexed.
Response
dataobjectoptional
keywordsarray of objectoptional
The keywords that were added or reactivated by this call.
idstringoptional
Unique identifier for the tracked keyword.
keywordstringoptional
The keyword text as stored (using the first spelling submitted).
Errors
400The request is missing a required parameter or is otherwise malformed.
401Missing, invalid, expired, or revoked API key.
403The key lacks the required scope, or isn't authorized for this client/location.
422The request is missing a required parameter or is otherwise malformed.
429Too many requests. Retry after the number of seconds in the Retry-After header.
post/api/v1/seo/keywords
Your API key
Request body*
{
  "data": {
    "keywords": [
      {
        "id": "cm_kw_abc123",
        "keyword": "dentist near me"
      }
    ]
  }
}
v1Resources/SEO/deleteRemove a tracked keyword

Stops tracking a keyword and deletes its stored ranking reports.

Remove a tracked keyword

delete/api/v1/seo/keywords
seo:write
Query parameters
keywordIdstringrequired
The tracked keyword to remove.
clientIdstringoptional
The keyword's owning location's client.
Response
dataobjectoptional
removedbooleanoptional
Always true — the keyword and its stored ranking reports were removed.
Errors
400The request is missing a required parameter or is otherwise malformed.
401Missing, invalid, expired, or revoked API key.
403The key lacks the required scope, or isn't authorized for this client/location.
404The resource wasn't found, or doesn't belong to your agency.
429Too many requests. Retry after the number of seconds in the Retry-After header.
delete/api/v1/seo/keywords
Your API key
keywordId *
clientId
{
  "data": {
    "removed": true
  }
}
v1Resources/SEO/patchUpdate a location's rank-grid config

Updates a location's rank-grid measurement config: grid size, distance between points, excluded grid points, or whether water grid points count toward stats. Pass only the fields you want to change. Water points are always scraped either way — includeWaterPoints only controls whether they're counted and shown. Read-side is covered by GET /api/v1/seo/keywords, whose response already includes includeWaterPoints.

Update a location's rank-grid config

patch/api/v1/seo/config
seo:write
Request body
locationIdstringrequired
The location to configure.
clientIdstringoptional
The location's client.
gridSizeinteger (3 | 5 | 7)optional
Grid size: 3, 5, or 7.
distanceKmnumberoptional
Distance in kilometers between adjacent grid points.
excludedarray of objectoptional
Grid points to exclude from measurement.
rowintegeroptional
Grid row index, 0-indexed.
colintegeroptional
Grid column index, 0-indexed.
includeWaterPointsbooleanoptional
Whether water grid points count toward stats.
Response
dataobjectoptional
configobjectoptional
The location's grid config after the update.
Errors
400The request is missing a required parameter or is otherwise malformed.
401Missing, invalid, expired, or revoked API key.
403The key lacks the required scope, or isn't authorized for this client/location.
429Too many requests. Retry after the number of seconds in the Retry-After header.
patch/api/v1/seo/config
Your API key
Request body*
{
  "data": {
    "config": {
      "id": "cm_cfg_abc123",
      "synupLocationId": "os_9f8e7d6c",
      "clientLocationId": "cm_loc_1",
      "agencyId": "cm_agency_1",
      "placeId": "ChIJtXC2rwgF9YgRNh1YJHlA4vQ",
      "centerLat": 30.2672,
      "centerLng": -97.7431,
      "gridSize": 5,
      "distanceKm": 1.5,
      "excludedPoints": [
        {
          "row": 4,
          "col": 4
        }
      ],
      "generationStartedAt": null,
      "lastRunStatus": "complete",
      "lastError": null,
      "includeWaterPoints": true,
      "lastGeneratedAt": "2026-02-01T06:00:00.000Z",
      "createdAt": "2026-01-10T09:00:00.000Z",
      "updatedAt": "2026-02-05T10:00:00.000Z"
    }
  }
}
v1Resources/SEO/postDownload a branded ranking report

Builds the branded, section-configurable ranking report — the same "Download Report" action in the app. Returns rendered HTML, not a binary PDF: rasterizing to PDF happens in the browser (no headless-Chrome renderer runs server-side), so turning this into an actual PDF file is on the caller, the same constraint the app's own UI has. Distinct from POST /api/v1/seo/reports/generate, which only enqueues a background rank-grid scan and returns no document.

Download a branded ranking report

post/api/v1/seo/reports
seo:read
Request body
locationIdstringrequired
The location to report on.
presetstringoptional
Date-range preset, e.g. "lastMonth" (default).
fromstringoptional
Custom range start (ISO date). Requires to.
tostringoptional
Custom range end (ISO date). Requires from.
keywordsarray of stringoptional
Limit to these tracked keyword ids. Omit for all.
sectionsobjectoptional
Which report sections to include, by key. Omitted keys use the report's own defaults.
coverPagebooleanoptional
executiveSummarybooleanoptional
performanceSnapshotbooleanoptional
agencyActivityLogbooleanoptional
gridRankSnapshotsbooleanoptional
keywordPerformanceTablebooleanoptional
monthOverMonthTrendbooleanoptional
competitorLandscapebooleanoptional
aiRecommendationsbooleanoptional
notesstringoptional
Free-text notes to include on the report.
localestringoptional
Language for the rendered report. Defaults to en.
Response
dataobjectoptional
htmlstringoptional
The rendered report as a self-contained HTML document.
filenamestringoptional
Suggested filename for the report.
Errors
400The request is missing a required parameter or is otherwise malformed.
401Missing, invalid, expired, or revoked API key.
403The key lacks the required scope, or isn't authorized for this client/location.
429Too many requests. Retry after the number of seconds in the Retry-After header.
502The report couldn't be built (a data-source failure, not a client error).
post/api/v1/seo/reports
Your API key
Request body*
{
  "data": {
    "html": "<style>#seo-report{width:100%;...}</style><div id=\"seo-report\"><section class=\"cover\"><h1>Ranking Analytics Report</h1><div class=\"biz\">Acme Dental — Downtown</div></section>...</div>",
    "filename": "Acme_Dental_Downtown_RankingReport_Jan2026_Feb2026.pdf"
  }
}
v1Resources/SEO/getGet the all-locations ranking roll-up

Returns one summary row per location that has at least one ranking report, optionally scoped to a client and/or location tags. A key restricted to specific clients must pass clientId.

Get the all-locations ranking roll-up

get/api/v1/seo/rollup
seo:read
Query parameters
clientIdstringoptional
Restrict results to one client. When your key is scoped to specific clients, this is required.
tagsstringoptional
Comma-separated location tag names. Only locations carrying at least one of these tags are included.
Response
dataobjectoptional
countnumberoptional
Number of rows returned.
rowsarray of objectoptional
One row per location with at least one ranking report.
idstringoptional
Unique identifier for the location.
locationNamestringoptional
The location's business name.
citystringoptional
City, or null.
avgRanknumberoptional
Mean average rank across the location's active keywords, or null.
top3Pctnumberoptional
Mean percent of grid points in the top 3 across the location's keywords, or null.
top10Pctnumberoptional
Mean percent of grid points in the top 10 across the location's keywords, or null.
bestKeywordobjectoptional
The location's best-ranking keyword, or null.
keywordstringoptional
avgRanknumberoptional
tagsarray of stringoptional
The location's internal tags.
Errors
401Missing, invalid, expired, or revoked API key.
403The key lacks the required scope, or isn't authorized for this client/location.
429Too many requests. Retry after the number of seconds in the Retry-After header.
get/api/v1/seo/rollup
Your API key
clientId
tags
{
  "data": {
    "count": 2,
    "rows": [
      {
        "id": "cm_loc_1",
        "locationName": "Acme Dental — Downtown",
        "city": "Austin",
        "avgRank": 4.2,
        "top3Pct": 44,
        "top10Pct": 78,
        "bestKeyword": {
          "keyword": "dentist near me",
          "avgRank": 4.2
        },
        "tags": [
          "priority",
          "downtown"
        ]
      }
    ]
  }
}
v1Resources/SEO/getGet a location's competitor leaderboard

Returns the businesses that dominate this location's ranking grid for its most recently tracked keyword, ranked by how much of the grid they occupy in the top 3. Includes your own business (isYou) for comparison, and tags entries already in your tracked-competitor registry with their registry ID.

Get a location's competitor leaderboard

get/api/v1/seo/competitors
seo:read
Query parameters
locationIdstringrequired
The location to look up.
clientIdstringoptional
The location's client.
Response
dataobjectoptional
countnumberoptional
Number of competitors returned.
competitorsarray of objectoptional
The competitor leaderboard, including your own business.
idstringoptional
An identifier for this business (its place ID, or a derived key when no place ID is available).
competitorIdstringoptional
ID in your tracked-competitor registry, present only when this business is already tracked there.
namestringoptional
Business name.
isYoubooleanoptional
Whether this row is your own business.
ratingnumberoptional
Star rating, or null.
reviewCountnumberoptional
Number of reviews, or null.
avgRanknumberoptional
Average rank across the grid points this business appeared at, or null.
top3Pctnumberoptional
Percent of grid points where this business ranks in the top 3, averaged across all tracked keywords.
gridPointsnumberoptional
Number of grid points this business appeared at.
categorystringoptional
Business category, or null.
websitestringoptional
Business website, or null (not returned by the local-search data source).
keywordBreakdownarray of objectoptional
This business's top-3 coverage broken out per tracked keyword.
keywordstringoptional
The tracked keyword.
top3Pctnumberoptional
Percent of that keyword's grid points where this business ranks in the top 3.
featureComparisonarray of objectoptional
Feature-by-feature comparison rows against your business, if any.
featurestringoptional
competitorbooleanoptional
youbooleanoptional
hoursComparisonarray of objectoptional
Business-hours comparison rows against your business, if any.
daystringoptional
competitorstringoptional
youstringoptional
Errors
400The request is missing a required parameter or is otherwise malformed.
401Missing, invalid, expired, or revoked API key.
403The key lacks the required scope, or isn't authorized for this client/location.
429Too many requests. Retry after the number of seconds in the Retry-After header.
get/api/v1/seo/competitors
Your API key
locationId *
clientId
{
  "data": {
    "count": 2,
    "competitors": [
      {
        "id": "cm_comp_1",
        "competitorId": "cm_competitor_1",
        "name": "Acme Dental — Downtown",
        "isYou": true,
        "rating": 4.6,
        "reviewCount": 210,
        "avgRank": 4.2,
        "top3Pct": 44,
        "gridPoints": 25,
        "category": "Dentist",
        "website": "https://acmedental.example.com",
        "keywordBreakdown": [
          {
            "keyword": "dentist near me",
            "top3Pct": 60
          }
        ],
        "featureComparison": [
          {
            "feature": "Online booking",
            "competitor": false,
            "you": true
          }
        ],
        "hoursComparison": [
          {
            "day": "Monday",
            "competitor": "9am–5pm",
            "you": "8am–6pm"
          }
        ]
      }
    ]
  }
}
get/api/v1/seo/competitors/trends
Your API key
locationId *
clientId
{
  "data": {
    "months": [
      "2026-01",
      "2026-02"
    ],
    "competitors": [
      {
        "placeId": "ChIJtXC2rwgF9YgRNh1YJHlA4vQ",
        "name": "Acme Dental — Downtown",
        "isYou": true,
        "monthly": [
          5.1,
          4.2
        ]
      }
    ]
  }
}
v1Resources/SEO/getGet businesses ranked at one grid point

Returns the local-search results captured at a single grid cell for one keyword, in rank order.

Get businesses ranked at one grid point

get/api/v1/seo/grid-point
seo:read
Query parameters
locationIdstringrequired
The location to look up.
keywordIdstringrequired
The tracked keyword whose grid this point belongs to.
rowintegerrequired
Grid row index of the point.
colintegerrequired
Grid column index of the point.
clientIdstringoptional
The location's client.
monthstringoptional
Pin the result to this month (YYYY-MM) instead of the keyword's latest measured report.
Response
dataobjectoptional
countnumberoptional
Number of businesses returned.
businessesarray of objectoptional
Businesses ranked at this grid point, in rank order.
idstringoptional
An identifier for this business (its place ID, name, or list position).
ranknumberoptional
This business's rank at this grid point (1 = top).
namestringoptional
Business name.
photoUrlstringoptional
Photo URL, or null (not returned by the local-search data source).
ratingnumberoptional
Star rating, or null.
reviewCountnumberoptional
Number of reviews, or null.
categorystringoptional
Business category, or null.
addressstringoptional
Street address, or null.
websitestringoptional
Business website, or null (not returned by the local-search data source).
isYoubooleanoptional
Whether this row is your own business.
Errors
400The request is missing a required parameter or is otherwise malformed.
401Missing, invalid, expired, or revoked API key.
403The key lacks the required scope, or isn't authorized for this client/location.
429Too many requests. Retry after the number of seconds in the Retry-After header.
get/api/v1/seo/grid-point
Your API key
locationId *
keywordId *
row *
col *
clientId
month
{
  "data": {
    "count": 1,
    "businesses": [
      {
        "id": "cm_biz_1",
        "rank": 3,
        "name": "Acme Dental — Downtown",
        "photoUrl": null,
        "rating": 4.6,
        "reviewCount": 210,
        "category": "Dentist",
        "address": "123 Main St, Austin, TX 78701",
        "website": null,
        "isYou": true
      }
    ]
  }
}
v1Resources/SEO/postRegenerate a location's ranking-grid report

Triggers a background rescrape of the local-search ranking grid for a location and/or a specific tracked keyword. This is fire-and-forget: the call returns immediately once the job is enqueued, and the new report lands asynchronously — poll GET /api/v1/seo/keywords (or GET /api/v1/seo/competitors, etc.) afterward to see it.

Regenerate a location's ranking-grid report

post/api/v1/seo/reports/generate
seo:write
Request body
locationIdstringoptional
Regenerate every active keyword's grid for this location. Optional if keywordId is given.
keywordIdstringoptional
Regenerate the grid for this one tracked keyword only. Optional if locationId is given.
monthstringoptional
Generate this month's ranking grid (YYYY-MM) instead of the current month.
clientIdstringoptional
The location's (or keyword's owning location's) client.
Response
dataobjectoptional
generatingbooleanoptional
Always true — a background generation job was enqueued.
Errors
400The request is missing a required parameter or is otherwise malformed.
401Missing, invalid, expired, or revoked API key.
403The key lacks the required scope, or isn't authorized for this client/location.
404The resource wasn't found, or doesn't belong to your agency.
429Too many requests. Retry after the number of seconds in the Retry-After header.
post/api/v1/seo/reports/generate
Your API key
Request body
{
  "data": {
    "generating": true
  }
}