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
/api/v1/seo/keywords{
"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
}
}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
/api/v1/seo/keywords{
"data": {
"keywords": [
{
"id": "cm_kw_abc123",
"keyword": "dentist near me"
}
]
}
}Stops tracking a keyword and deletes its stored ranking reports.
Remove a tracked keyword
/api/v1/seo/keywords{
"data": {
"removed": true
}
}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
/api/v1/seo/config{
"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"
}
}
}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
/api/v1/seo/reports{
"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"
}
}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
/api/v1/seo/rollup{
"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"
]
}
]
}
}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
/api/v1/seo/competitors{
"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"
}
]
}
]
}
}Returns each competitor's average rank per month across this location's tracked ranking history (up to the last 12 measured months), for the rank-trend chart. Includes your own business (isYou).
Get competitor rank trends over time
/api/v1/seo/competitors/trends{
"data": {
"months": [
"2026-01",
"2026-02"
],
"competitors": [
{
"placeId": "ChIJtXC2rwgF9YgRNh1YJHlA4vQ",
"name": "Acme Dental — Downtown",
"isYou": true,
"monthly": [
5.1,
4.2
]
}
]
}
}Returns the local-search results captured at a single grid cell for one keyword, in rank order.
Get businesses ranked at one grid point
/api/v1/seo/grid-point{
"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
}
]
}
}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
/api/v1/seo/reports/generate{
"data": {
"generating": true
}
}