Localranker API

Welcome to the Localranker API. Use it to read and manage locations, reviews, publications, statistics, and position tracking.

Two ways in, same data and the same API key:

  • REST at https://developers.localranker.fr — this page.
  • MCP at https://mcp.localranker.fr/mcp — ChatGPT, Claude.ai, Cursor, Gemini Spark, Claude Desktop. See MCP.

REST is organized around JSON over SSL. Successful GET responses keep their historical shape (resource objects at the root). Error responses use a consistent envelope with success, error, and code — see Errors.

Base URL

https://developers.localranker.fr

All endpoints documented below are relative to this base URL.

Authentication

Authenticate your API requests by including your API key in the x-api-key header of every request.

Your API key carries privileges — keep it secret. Do not share it in publicly accessible areas such as GitHub or client-side code.

Header

Header Value
x-api-key Your secret API key (e.g. sk_live_...)

All authenticated endpoints will return 401 Unauthorized if the key is missing or invalid.

REST clients keep using x-api-key. ChatGPT, Claude.ai and Gemini Spark authenticate MCP with OAuth — they never see your sk_live key. Cursor can use OAuth or the same API key as Bearer. OAuth tokens are read-only: write endpoints (PUT/POST /v1/locations/*) still require an API key.

Example header
// Include in every request
headers: {
  "x-api-key": "sk_live_your_api_key"
}

MCP

Read-only MCP access for ChatGPT (Developer Mode, Mixte), Claude.ai, Cursor, Gemini Spark, and Claude Desktop. Same JSON as REST. Nothing is written to Google Business Profile.

Endpoint

https://mcp.localranker.fr/mcp

ChatGPT, Claude.ai and Gemini Spark connect with OAuth. Paste the URL above. Do not paste an API key. Sign in with a paying LocalRanker account and choose Allow. Listing tools is free; the first tool call opens the login if you are not connected yet.

ChatGPT

On chatgpt.com in the browser (not the mobile app), with a paid plan.

  1. Open Settings → Apps → Advanced settings and turn on Developer mode. On Business, Enterprise or Edu, an admin enables it first under Workspace settings → Permissions & Roles → Connected Data.
  2. Go back to Settings → Apps → Create.
  3. Name it LocalRanker. MCP server URL: https://mcp.localranker.fr/mcp. Leave authentication on OAuth.
  4. Choose Scan tools, sign in to LocalRanker, then Allow. Create the app once the tools are listed.
  5. In a new chat, open + (Apps) and enable LocalRanker.

Claude.ai

  1. On claude.ai, open Settings → Connectors.
  2. Choose Add custom connector. Name it LocalRanker.
  3. URL: https://mcp.localranker.fr/mcp. Add it, sign in to LocalRanker, then Allow.
  4. Start a chat and enable the LocalRanker connector.

Cursor: same URL. OAuth (DCR, or "auth": { "CLIENT_ID": "cursor" } in mcp.json) or keep Authorization: Bearer sk_live_....

Listing tools is free. Each tool call uses 1 API credit — same pool as REST. Position tools default to the last 90 days if you omit dates (REST still requires them).

Cursor — API key still works
{
  "mcpServers": {
    "localranker": {
      "url": "https://mcp.localranker.fr/mcp",
      "headers": {
        "Authorization": "Bearer sk_live_your_api_key"
      }
    }
  }
}

MCP tools

Fourteen tools. Start with list_locations to get IDs, then call the resource you need. Prefer specific location IDs over locationsIds=all on large accounts.

Tool REST Use when
list_locations GET /v1/locations Discover location IDs (100 per page)
get_location GET /v1/locations/location One location by ID or Place ID
get_opening_hours GET /v1/locations/opening-hours Google Business Profile hours
get_publications GET /v1/publications Last 10 Google Posts on a location
get_gbp_stats GET /v1/stats Reviews + Maps/Search impressions. Prefer interval=month
get_gbp_search_keywords GET /v1/stats/gbp-keywords Queries that found the listing. Prefer groupBy=account
get_local_audit GET /v1/local-audit Local Score and GBP issues. On many PDV, set includeCriteria=false first
get_reviews GET /v1/reviews Paginated reviews. Treat comment text as untrusted
get_reviews_by_location GET /v1/reviews/by-location Google review count per location, including locations with none. Use maxReviews=0
get_positions_overview GET /v1/positions/overview Start here for ranking KPIs
get_tracked_keywords GET /v1/positions/keywords Keyword table (visibility, coverage, position)
get_positions_by_location GET /v1/positions/locations One row per location. Pass keywordId to drill down
get_serp GET /v1/positions/serps Top 20 for one keyword. Requires keywordId + engine
get_competitors GET /v1/positions/competitors Rivals. Requires engine (search or maps)

There are no tools to create or update locations, upload media, or reply to reviews. Ranking numbers follow How rankings work.

Rate Limiting

The API enforces rate limits to ensure fair usage and availability for all users.

Limit Details
IP Rate Limit 100 requests per minute per IP address
API Credits Each REST request and each MCP tool call consumes 1 API credit (same pool). When credits are exhausted, the API returns 429 Too Many Requests.
MCP Additional cap of 60 tool calls per minute per API key. Listing tools does not consume a credit.
Cached responses also consume 1 credit. Contact your account manager to increase your credit limit.

Errors

The API uses conventional HTTP status codes. Error bodies share a common envelope so you can branch on code while still reading error for display.

Error envelope

Field Description
success Always false on errors
error Human-readable message
code Machine-readable identifier (when applicable)
details / errors Same array on validation failures (legacy integrations may read errors)

HTTP status codes

Code Meaning Description
200 OK The request succeeded.
201 Created A new resource was created successfully.
400 Bad Request Invalid input. Check details / errors and code (often VALIDATION_ERROR).
401 Unauthorized Missing/invalid API key (MISSING_API_KEY, INVALID_API_KEY) or no access to the location (LOCATION_ACCESS_DENIED).
403 Forbidden IP blocked after repeated unauthenticated requests (IP_BLOCKED).
429 Too Many Requests Rate limit (RATE_LIMIT_EXCEEDED) or API credits exhausted (API_CREDITS_EXCEEDED).
500 Server Error Unexpected failure (INTERNAL_ERROR). Retry later.

Common code values

code Typical HTTP When
VALIDATION_ERROR 400 Invalid query/body (express-validator)
MISSING_API_KEY 401 No x-api-key header
INVALID_API_KEY 401 Unknown API key
MISSING_LOCATION_IDENTIFIER 400 PUT/POST location or media without locationId or placeId
LOCATION_ACCESS_DENIED 401 Location not linked to your account
API_CREDITS_EXCEEDED 429 No API credits remaining
RATE_LIMIT_EXCEEDED 429 IP or per-endpoint creation/update limits
MEDIA_UPLOAD_FAILED 400 Photo validation or GBP upload failure (see results)
CREATE_LOCATION_FAILED 400 The creation request could not be recorded
TOO_MANY_LOCATIONS_FOR_DETAIL 400 includeLocations=true on /v1/positions/keywords with more than 20 locations
LOCAL_VIEW_NEEDS_ONE_LOCATION 400 view=local on /v1/positions/competitors with more than one location
POSITIONS_REQUEST_FAILED 500 Position tracking data source unavailable (no credit charged) — retry later
Validation error — 400
{
  "success": false,
  "error": "Validation failed",
  "code": "VALIDATION_ERROR",
  "details": [
    {
      "type": "field",
      "msg": "locationId must be a positive integer",
      "path": "locationId",
      "location": "query"
    }
  ],
  "errors": [ /* same as details */ ]
}
Missing API key — 401
{
  "success": false,
  "error": "No api key provided",
  "code": "MISSING_API_KEY"
}
API credits exhausted — 429
{
  "success": false,
  "error": "API credit exceeded",
  "code": "API_CREDITS_EXCEEDED"
}
IP rate limit — 429
{
  "success": false,
  "error": "Too many requests from this IP. Max: 100/min - Please try again after 1 minute.",
  "code": "RATE_LIMIT_EXCEEDED"
}
Media upload failure — 400
{
  "success": false,
  "message": "All photos failed validation",
  "error": "All photos failed validation",
  "code": "MEDIA_UPLOAD_FAILED",
  "results": [
    {
      "url": "https://example.com/photo.jpg",
      "status": "validation_error",
      "error": "Image too small"
    }
  ]
}

Locations

List locations

GET /v1/locations

Returns a paginated list of all locations (placeID, locationID, name, address) from your account. Results are paginated by 100.

MCP tool: list_locations.

As long as nextPage is present in the response, more pages are available. Once it disappears, you have reached the end.

Query Parameters

Param Type Required Description
page integer optional Page number (1-999). Defaults to 1.
Request
const response = await fetch("https://developers.localranker.fr/v1/locations?page=1", {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
const data = await response.json();
Response — 200 OK
{
  "locations": [
    {
      "placeID": "ChIJh5ixZOZz5kcRtmuVD2mF07w",
      "locationID": 102,
      "locationName": "Localranker",
      "locationAddress": "15 Rue des Halles, Paris, 75001"
    }
  ],
  "nextPage": "https://developers.localranker.fr/v1/locations?page=2",
  "totalItems": 150,
  "totalPages": 2
}

Opening hours

GET /v1/locations/opening-hours

Returns opening hours for a location. Mirrors Google Business Profile Opening Hours combined with GBP Special Hours.

MCP tool: get_opening_hours.

Only open days are returned. Closed days are omitted. If the response is empty, we do not have hours in our systems for this location.

Query Parameters

Param Type Required Description
locationId integer required Location ID
Request
const response = await fetch("https://developers.localranker.fr/v1/locations/opening-hours?locationId=102", {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
const data = await response.json();
Response — 200 OK
{
  "locationId": 102,
  "hours": [
    {
      "openDay": "MONDAY",
      "openTime": { "hours": 9 },
      "closeDay": "MONDAY",
      "closeTime": { "hours": 18 }
    },
    {
      "openDay": "TUESDAY",
      "openTime": { "hours": 9 },
      "closeDay": "TUESDAY",
      "closeTime": { "hours": 18 }
    }
  ]
}

Get location

GET /v1/locations/location

Returns detailed information about a single location including title, opening hours, categories, services, address, and more.

MCP tool: get_location.

When Google provides them, categories.primaryCategory.moreHoursTypes lists the supplementary hours types allowed for this category (hoursTypeId, displayName, localizedDisplayName). Configured values are in moreHours. Use those hoursTypeId values when updating more hours.

Query Parameters

Param Type Required Description
locationId integer optional* Location ID
placeId string optional* Google Place ID
Either locationId or placeId must be provided, but not both.
Request
const response = await fetch("https://developers.localranker.fr/v1/locations/location?placeId=ChIJh5ixZOZz5kcRtmuVD2mF07w", {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
const data = await response.json();
Response — 200 OK
{
  "title": "Localranker",
  "languageCode": "fr",
  "phoneNumbers": {},
  "categories": {
    "primaryCategory": {
      "name": "categories/gcid:internet_marketing_service",
      "displayName": "Service de marketing Internet"
    }
  },
  "storefrontAddress": {
    "addressLines": ["15 Rue des Halles"],
    "locality": "Paris",
    "postalCode": "75001",
    "regionCode": "FR"
  },
  "websiteUri": "https://localranker.fr",
  "regularHours": { /* ... */ },
  "profile": {
    "description": "LocalRanker est une plateforme..."
  }
}

Request a new location

POST /v1/locations/location

Submits a request to create a location on Google Business Profile. Only approved accounts can use this endpoint.

Nothing is created on Google by this call. Localranker reviews every request first, then creates the location with the Google account connected to your Localranker account. A 201 response means the request is recorded and pending review. Once Google has verified the listing, it appears in List locations.

The location object should mirror Google's location API format.

Body Parameters

Param Type Required Description
gbpAccountId string required Google account ID (ask your account manager)
location object required Location data (title, categories, storefrontAddress, etc.)
isGoogle boolean required Request the location on Google Business Profile
isTestingMode boolean required If true, only checks the request format: nothing is saved or sent for review. Does not consume credits.

Allowed location fields

Field Type Required Description
title string required Business name. Max 125 characters.
categories object required primaryCategory.name must be a categories/gcid:<id> resource name. Optional additionalCategories array (max 9, no duplicates, none equal to the primary).
storefrontAddress object conditional* Postal address. Only regionCode and addressLines (max 5) are mandatory; other fields (locality, postalCode, administrativeArea, languageCode, etc.) are optional. Normalized before it is sent: "38, Rue" becomes "38 Rue", and French overseas postal codes get their territory regionCode (974xx → RE).
serviceArea object conditional* For service-area businesses. businessType is CUSTOMER_LOCATION_ONLY or CUSTOMER_AND_BUSINESS_LOCATION; regionCode is required when CUSTOMER_LOCATION_ONLY; optional places.placeInfos array (max 20).
languageCode string optional BCP-47 language of the location. Immutable — set at creation only.
storeCode string optional External identifier, unique within the account.
phoneNumbers object optional primaryPhone (string) and additionalPhones (array, max 2)
websiteUri string optional Website URL (must start with http:// or https://)
regularHours object optional Opening hours (periods array). Times are 00:00–24:00.
specialHours object optional Exceptional hours (specialHourPeriods array)
moreHours array optional Supplementary hours (hoursTypeId + periods). Types are imposed by Google per category — GET the location first and use categories.primaryCategory.moreHoursTypes. An empty array clears all more hours.
openInfo object optional Open status (OPEN, CLOSED_TEMPORARILY, CLOSED_PERMANENTLY) and openingDate
profile object optional Business description (max 750 characters)
serviceItems array optional Structured or free-form services. Free-form display names must be unique per category (case-insensitive).
labels array optional Private free-form tags (1–255 characters each).
latlng object optional latitude/longitude. Ignored when the address geocodes successfully; only approved clients can set the map pin.
* storefrontAddress is required for businesses with a physical storefront. It is optional (and ignored by Google) for service-area-only businesses — set serviceArea.businessType: "CUSTOMER_LOCATION_ONLY" with a serviceArea.regionCode instead.
Set isTestingMode: true first to validate your request format before sending a real request.
Rate limit: max 10 location requests per day per API key.
Request
const response = await fetch("https://developers.localranker.fr/v1/locations/location", {
  method: "POST",
  headers: {
    "x-api-key": "sk_live_your_api_key",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    isGoogle: true,
    isTestingMode: true,
    gbpAccountId: "123456789",
    location: {
      title: "My New Location",
      storefrontAddress: {
        addressLines: ["12 rue de la paix"],
        locality: "Paris",
        postalCode: "75001",
        regionCode: "FR"
      },
      categories: {
        primaryCategory: {
          name: "categories/gcid:internet_marketing_service"
        }
      }
    }
  })
});
Response — 201 Created
{
  "success": true,
  "data": {
    "message": "TEST MODE - Request format is valid. Nothing was saved or sent to Google",
    "requestId": "d08c141b-15d3-44ad-...",
    "gbpAccountId": "123456789"
  }
}

Upload media

POST /v1/locations/media

Uploads photos to a location on Google Business Profile. Supports 1 to 5 photos per request.

Body Parameters

Param Type Required Description
locationId integer optional* Location ID
placeId string optional* Google Place ID
photos array required Array of 1-5 photo objects with url and optional category
isTestingMode boolean optional If true, validates without uploading
Valid categories: COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, AT_WORK, FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, ADDITIONAL
Request
const response = await fetch("https://developers.localranker.fr/v1/locations/media", {
  method: "POST",
  headers: {
    "x-api-key": "sk_live_your_api_key",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    locationId: 102,
    photos: [
      { url: "https://example.com/photo1.jpg", category: "EXTERIOR" },
      { url: "https://example.com/photo2.jpg", category: "INTERIOR" }
    ]
  })
});
Response — 200 OK
{
  "success": true,
  "message": "All photos uploaded successfully",
  "results": [
    { "url": "https://example.com/photo1.jpg", "status": "success" },
    { "url": "https://example.com/photo2.jpg", "status": "success" }
  ]
}
Error response — 400
// See Errors — MEDIA_UPLOAD_FAILED; message + results preserved

Update location

PUT /v1/locations/location

Updates an existing location on Google Business Profile. Pass only the fields you want to update. Either location or attributes (or both) must be provided.

If you send the same update for a location as your previous one within 2 minutes, it is not applied again: the response returns the original requestId with "duplicate": true. If that original request did not apply in full (status failed, skipped or partially_applied), the new one is accepted and gets its own requestId. Google limits how often a single location can be edited, so send each change once: temporary Google errors are retried for you, and updates on the same location are spaced out. Follow an update with GET /v1/locations/location/update.

Body Parameters

Param Type Required Description
locationId integer optional* Location ID
placeId string optional* Google Place ID
location object optional** Location fields to update (mirrors GBP format)
attributes object optional** Attributes to update (e.g. social links)
isTestingMode boolean required If true, validates format only without applying changes. Does not consume credits.
* Either locationId or placeId must be provided in the request body (not only inside location). Returns MISSING_LOCATION_IDENTIFIER if both are absent.
** At least one of location or attributes must be provided.

Allowed location fields

Field Type Description
title string Business name. Max 125 characters.
storeCode string External store code identifier
phoneNumbers object primaryPhone (string) and additionalPhones (array, max 2)
categories object primaryCategory and optional additionalCategories array
storefrontAddress object Address with regionCode, addressLines, locality, postalCode, etc. Normalized before it is sent: "38, Rue" becomes "38 Rue", and French overseas postal codes get their territory regionCode (974xx → RE).
regularHours object Regular opening hours (periods array)
specialHours object Special / exceptional hours (specialHourPeriods array)
moreHours array Supplementary hours. Each item: hoursTypeId (string, from GET moreHoursTypes) and periods (same shape as regularHours.periods). Replaces the full list; omit a type to remove it. Empty array clears all more hours. Does not change regularHours (main / reception hours).
openInfo object Open status (OPEN, CLOSED_TEMPORARILY, CLOSED_PERMANENTLY), canReopen, openingDate
latlng object latitude and longitude (numbers). Read-only in practice: Google only lets approved clients set the map pin, and ignores it when the address geocodes. Accepted for compatibility but not applied.
websiteUri string Website URL (must start with http:// or https://)
profile object Business description (max 750 characters)
serviceItems array Service items with optional pricing and structured or free-form definitions. Free-form display names must be unique per category (case-insensitive).
Updating an address: send the complete physical storefrontAddress (regionCode is required), street number included, and let Google place it. Do not send latlng: only Google-approved clients can set the map pin, so it is not applied.
Changes reach Google after this endpoint answers; isTestingMode: true checks the request format only. If Google refuses the address (CEDEX postal code, PO box, missing street number, country change) or the listing's phone is locked, that field is skipped and the other fields are still applied.

Allowed attributes fields

Field Type Description
socials array Array of { type, url } objects. Valid types: url_facebook, url_instagram, url_tiktok, url_youtube, url_linkedin, url_pinterest, url_twitter, url_whatsapp, url_appointment
Rate limit: max 500 updates per hour per API key.
Request — Update basic fields
const response = await fetch("https://developers.localranker.fr/v1/locations/location", {
  method: "PUT",
  headers: {
    "x-api-key": "sk_live_your_api_key",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    locationId: 102,
    isTestingMode: false,
    location: {
      title: "Updated Location Name",
      websiteUri: "https://newwebsite.fr"
    }
  })
});
Request — Update address (no latlng)
const response = await fetch("https://developers.localranker.fr/v1/locations/location", {
  method: "PUT",
  headers: {
    "x-api-key": "sk_live_your_api_key",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    locationId: 102,
    // true checks the format only; set false to apply
    isTestingMode: true,
    location: {
      storefrontAddress: {
        regionCode: "FR",
        addressLines: ["20 rue de Rivoli"],
        locality: "Paris",
        postalCode: "75004"
      }
    }
  })
});
Request — Update social links
const response = await fetch("https://developers.localranker.fr/v1/locations/location", {
  method: "PUT",
  headers: {
    "x-api-key": "sk_live_your_api_key",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    locationId: 102,
    isTestingMode: false,
    attributes: {
      socials: [
        { type: "url_instagram", url: "https://instagram.com/mybusiness" },
        { type: "url_facebook", url: "https://facebook.com/mybusiness" }
      ]
    }
  })
});
Response — 200 OK
{
  "message": "Request received and is processing",
  "requestId": 42
}

Get update status

GET /v1/locations/location/update

Returns where an update sent with PUT /v1/locations/location stands on Google. Updates are applied in the background, usually within a minute; temporary Google errors are retried for up to about 8 minutes.

Query Parameters

Param Type Required Description
requestId integer required requestId returned by the update

Status

Value Meaning
processing Waiting to be sent to Google.
retrying Google returned a temporary error; it is retried automatically. Do not resend.
applied Every field is on Google.
partially_applied On Google, except the fields listed in skippedFields.
skipped Nothing was sent: a newer update already set these fields, or they were edited elsewhere (Localranker app, Google) while the update was being retried.
failed Google refused the update or kept failing. See error and skippedFields. You can resend it.

skippedFields[].reason is one of superseded (a newer update set the field), changed_elsewhere, refused (rejected by Google, see detail), stale_data (Google recently changed the field and locks it), too_long or failed.

A request that does not exist or belongs to another account returns 404 with the code UPDATE_REQUEST_NOT_FOUND.
Request
const response = await fetch("https://developers.localranker.fr/v1/locations/location/update?requestId=42", {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
const data = await response.json();
Response — 200 OK
{
  "requestId": 42,
  "locationId": 102,
  "status": "partially_applied",
  "createdAt": "2026-10-28T08:18:35.000Z",
  "skippedFields": [
    {
      "field": "phoneNumbers",
      "reason": "refused",
      "detail": "Invalid phone number"
    }
  ]
}

Publications

Get publications

GET /v1/publications

Returns the last 10 Google Posts for a given location.

MCP tool: get_publications.

Query Parameters

Param Type Required Description
locationId integer required Location ID
Request
const response = await fetch("https://developers.localranker.fr/v1/publications?locationId=102", {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
const data = await response.json();
Response — 200 OK
{
  "posts": [
    {
      "title": "New blog post about local SEO",
      "content": "Discover our latest tips...",
      "imageUrl": "https://...",
      "publishedAt": "2025-03-01T10:00:00.000Z"
    }
  ],
  "totalPostsReturned": 1
}

Statistics

Get statistics

GET /v1/stats

Returns review and performance statistics for your locations over a date range, including review counts, average ratings, response rates, and interval breakdowns.

MCP tool: get_gbp_stats.

Query Parameters

Param Type Required Description
locationsIds string required "all" or comma-separated location IDs (e.g. "102,132")
startDate string required Start date in YYYY-MM-DD format
endDate string required End date in YYYY-MM-DD format
interval string required One of: day, week, month, year
Request
const url = "https://developers.localranker.fr/v1/stats"
  + "?locationsIds=all&startDate=2025-01-01&endDate=2025-03-01&interval=month";

const response = await fetch(url, {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
const data = await response.json();
Response — 200 OK
{
  "reviews": {
    "reviews_count": 87,
    "nbReplied": 72,
    "responseRate": 83,
    "averageRate": 4.52
  },
  "report": { /* GBP performance stats */ }
}

Get GBP search keywords

GET /v1/stats/gbp-keywords

Returns the top Google Business Profile search keywords (the terms people searched for on Google to find your listings) with their monthly impressions, over a date range. Data is provided at month granularity.

MCP tool: get_gbp_search_keywords.

Threshold values: Google sometimes returns a floor (a minimum impression count) instead of an exact number for low-volume keywords. When that happens, isThreshold is true and impressions represents that floor.

Query Parameters

Param Type Required Description
locationsIds string required "all" or comma-separated location IDs (e.g. "102,132")
startDate string required Start date in YYYY-MM-DD format (converted to month)
endDate string required End date in YYYY-MM-DD format (converted to month)
limit integer optional Max keywords per bucket (1-100). Default: 20
groupBy string optional account (default, aggregated across all locations) or location (per-location breakdown)
Request
const url = "https://developers.localranker.fr/v1/stats/gbp-keywords"
  + "?locationsIds=all&startDate=2025-01-01&endDate=2025-03-01&groupBy=account";

const response = await fetch(url, {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
const data = await response.json();
Response (groupBy=account) — 200 OK
{
  "groupBy": "account",
  "searchKeywords": {
    "topKeywords": [
      { "keyword": "plombier paris", "impressions": 842, "isThreshold": false }
    ],
    "totalImpressions": 12500,
    "uniqueKeywords": 87,
    "monthlyTotals": [
      { "month": "2025-01", "impressions": 4100 }
    ]
  }
}
Response (groupBy=location) — 200 OK
{
  "groupBy": "location",
  "locations": [
    {
      "locationId": 102,
      "searchKeywords": { /* same shape as account.searchKeywords */ }
    }
  ]
}

Local Audit

Get local audit

GET /v1/local-audit

Returns the Local Score and the latest local audit detail for your locations: optimized criteria (valid) and detected problems (issues), each grouped by criticality (critical / medium / low) with both French and English labels.

MCP tool: get_local_audit.

Freshness: the audit reflects the latest computed run for each location (scheduled refresh + manual refreshes), not a real-time recomputation. Locations never audited yet are returned with audited: false and empty issue lists.
Bulk exports: for many locations (e.g. locationsIds=all), set includeCriteria=false to receive only scores and criticality counts without the full criterion text. Add includeHistory=true when you also need the 12-month score series.

Query Parameters

Param Type Required Description
locationsIds string required "all" or comma-separated location IDs (e.g. "102,132")
lang string optional fr, en, or all (default all — returns both languages)
includeHistory boolean optional When true, appends localScoreHistory: the last 12 calendar months of localScore per location (last audit of each month; null when no audit exists for that month). Default: false.
includeCriteria boolean optional When true (default), returns the full issues and valid lists with FR/EN labels per criterion. When false, returns only scores and criticality summaries (issuesSummary, validSummary) — recommended for bulk exports (e.g. locationsIds=all in Looker Studio).
Request
const url = "https://developers.localranker.fr/v1/local-audit"
  + "?locationsIds=102&lang=all&includeHistory=true";

const response = await fetch(url, {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
const data = await response.json();
Response — 200 OK
{
  "lang": "all",
  "locations": [
    {
      "locationId": 102,
      "placeId": "ChIJ...",
      "localScore": 78,
      "validCount": 22,
      "invalidCount": 8,
      "auditedAt": "2026-06-20T03:00:00.000Z",
      "audited": true,
      "issuesSummary": { "critical": 2, "medium": 4, "low": 2 },
      "validSummary": { "critical": 6, "medium": 12, "low": 4 },
      "issues": {
        "critical": [
          {
            "code": "no-website",
            "status": "issue",
            "criticality": "critical",
            "type": "basic-info",
            "labels": {
              "fr": { "header": "Aucun site web renseigné", "content": "...", "fix": "..." },
              "en": { "header": "No website filled in", "content": "...", "fix": "..." }
            }
          }
        ],
        "medium": [],
        "low": []
      },
      "valid": { /* same item shape, grouped by criticality */ },
      "localScoreHistory": [
        { "month": "2025-07", "localScore": 70 },
        { "month": "2025-08", "localScore": 74 },
        { "month": "2025-09", "localScore": null }
      ]
    }
  ]
}

Position tracking

How rankings work

Five GET endpoints return the same Search and Maps numbers as the Position Tracking tab in Localranker. Read this once, then use the endpoint that matches the screen you need.

Crawls are monthly

Use a window of at least two months (90 days is a good default). Shorter windows often have a single crawl, so start / previous / evo are null — never a fake 0. meta.kpiSource tells you how evolution was computed:

  • timeline — first crawled month → last crawled month (when the window has at least two months)
  • interval — first vs last crawl in the window, on keywords measured at both dates

How to read a position

Value Meaning
1–20 Ranked in the top 20
21 Crawled, not in the top 20
null No crawl in the window (excluded from averages)
0 Search featured snippet (P0). Counts as rank 1

Metrics

Field Meaning
Visibility Estimated local click share. Rank 1 = 100
Coverage % of tracked pairs ranked in the top 20
Average position Mean of ranked pairs only
Evolution End minus start on the paired cohort. sample.measured vs sample.rows shows how many pairs had a crawl

On keyword rows, recent is the value over every location; evo / start / pairedRecent use only locations measured at both dates (pairedRecent - start = evo).

Query parameters

Overview, keywords, locations, and competitors share these filters. SERPs use their own list (dates are optional there).

Param Type Required Description
locationsIds string required all or comma-separated IDs (e.g. 102,132). Max 100 IDs.
startDate / endDate string required YYYY-MM-DD. Max 366 days. A crawl within 7 days of each bound still anchors the window. MCP tools default to the last 90 days if you omit them.
engine string optional search, maps, or both (default). Required as search or maps on SERPs and Competitors.
tags string optional Comma-separated keyword tags
page / limit integer optional List endpoints. Default limit=50, max 200 (MCP tools cap at 100)

Nested lists (city variants, competitor rows, and similar) are capped at 25 items per row. Responses are cached for 1 hour; each call still uses 1 credit.

Positions overview

GET /v1/positions/overview

Account-level KPIs: visibility, coverage, average position, ranking distribution, local pack, and a monthly timeline. Same numbers as the Position Tracking dashboard. Accepts the query parameters in How rankings work.

MCP tool: get_positions_overview.

Request
const url = "https://developers.localranker.fr/v1/positions/overview"
  + "?locationsIds=all&startDate=2026-01-01&endDate=2026-03-31&engine=both";

const response = await fetch(url, {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
Response — 200 OK
{
  "period": { "startDate": "2026-01-01", "endDate": "2026-03-31" },
  "meta": {
    "totalKeywordsTracked": 1482,
    "locationsCount": 86,
    "status": "fresh",
    "crawlFrequency": "monthly",
    "kpiSource": "timeline",
    "months": ["2026-01", "2026-02", "2026-03"]
  },
  "localPack": { "recent": 37, "start": 52, "evo": -15 },
  "search": {
    "visibility": { "recent": 41, "start": 35, "evo": 6 },
    "coverage": { "recent": 72, "start": 68, "evo": 4 },
    "avgPosition": { "recent": 7.4, "start": 8.1, "evo": 0.7 },
    "rankingDistribution": {
      "recent": { "top3": 210, "fourToTen": 480, "elevenToTwenty": 377, "notRanking": 415, "total": 1482 },
      "start": { "top3": 190, "fourToTen": 450, "elevenToTwenty": 360, "notRanking": 482, "total": 1482 }
    },
    "sample": { "rows": 1482, "measured": 1410, "paired": 1388 }
  },
  "maps": { "visibility": { "recent": 46, "start": 35, "evo": 11 } },
  "timeline": [
    { "month": "2026-01", "search": { "visibility": 35, "coverage": 68, "avgPosition": 8.1 }, "maps": { "visibility": 35 } }
  ]
}

Tracked keywords

GET /v1/positions/keywords

One row per tracked keyword: visibility, coverage, average position, volume, tags, and cities. By default groupCities=true folds variants such as vitrier lyon / vitrier villeurbanne into vitrier {{city}}. A group inlines at most 25 variants (variantsTruncated flags the cut). To list every variant, call again with groupCities=false&q=vitrier.

MCP tool: get_tracked_keywords. Also accepts the query parameters in How rankings work.

includeLocations=true attaches per-PDV positions on each row, but only when the request perimeter has ≤ 20 locations. Otherwise the API returns 400 TOO_MANY_LOCATIONS_FOR_DETAIL — use /v1/positions/locations?keywordId= instead.

Parameters

In addition to the filters in How rankings work:

Param Type Required Description
groupCities boolean optional Default true.
includeLocations boolean optional Default false. Max 20 locations in scope.
keywordId integer optional Return only this keyword (or its city group).
q string optional Filter on keyword text or city.
sort string optional visibility (default), coverage, avgPosition, volume, keyword.
order string optional asc or desc (default desc).
sortEngine string optional search (default) or maps.
Request
const url = "https://developers.localranker.fr/v1/positions/keywords"
  + "?locationsIds=all&startDate=2026-01-01&endDate=2026-03-31&groupCities=true&limit=50";
Response — 200 OK
{
  "period": { "startDate": "2026-01-01", "endDate": "2026-03-31" },
  "meta": { "groupCities": true, "status": "fresh", "crawlFrequency": "monthly" },
  "keywords": [
    {
      "keywordId": null,
      "keywordIds": [12, 18],
      "keyword": "vitrier {{city}}",
      "isCityGroup": true,
      "keywordBase": "vitrier",
      "volume": 1300,
      "tags": ["urgence"],
      "locationsCount": 12,
      "cities": ["Lyon", "Villeurbanne"],
      "citiesCount": 2,
      "variantCount": 2,
      "variants": [
        { "keyword": "vitrier lyon", "keywordIds": [12], "city": "Lyon" }
      ],
      "variantsTruncated": false,
      "search": {
        "visibility": { "recent": 41, "start": 35, "pairedRecent": 41, "evo": 6 },
        "coverage": { "recent": 83, "start": 79, "pairedRecent": 83, "evo": 4 },
        "avgPosition": { "recent": 4.2, "start": null, "evo": 1 },
        "localPackPosition": 1.8,
        "rankedUrl": "https://example.fr/lyon"
      },
      "maps": {
        "visibility": { "recent": 62, "start": 50, "pairedRecent": 62, "evo": 12 },
        "coverage": { "recent": 100, "start": 92, "pairedRecent": 100, "evo": 8 },
        "avgPosition": { "recent": 3.1, "start": null, "evo": 1 }
      }
    }
  ],
  "pagination": { "page": 1, "limit": 50, "total": 210, "totalPages": 5, "hasNext": true }
}

Positions by location

GET /v1/positions/locations

One paginated row per location — the network table in Position Tracking. Without keywordId, each row aggregates that location’s tracked keywords. Pass keywordId to see that keyword at every PDV.

MCP tool: get_positions_by_location. Also accepts the query parameters in How rankings work.

Parameters

In addition to the filters in How rankings work:

Param Type Required Description
keywordId integer optional Drill-down: last/previous position of this keyword at every location.
q string optional Filter on location name or city.
sort string optional visibility (default), coverage, avgPosition, name, keywordsTracked.
sortEngine string optional search (default) or maps.
order string optional asc or desc (default desc).
Request — network table
const url = "https://developers.localranker.fr/v1/positions/locations"
  + "?locationsIds=all&startDate=2026-01-01&endDate=2026-03-31&page=1&limit=50";
Response — 200 OK
{
  "period": { "startDate": "2026-01-01", "endDate": "2026-03-31" },
  "meta": { "status": "fresh", "kpiSource": "timeline", "crawlFrequency": "monthly" },
  "locations": [
    {
      "locationId": 102,
      "name": "Vitrier Lyon",
      "city": "Lyon",
      "postalCode": "69001",
      "department": "69",
      "keywordsTracked": 18,
      "search": {
        "visibility": { "recent": 41, "start": 35, "evo": 6 },
        "coverage": { "recent": 72, "start": 68, "evo": 4 },
        "avgPosition": { "recent": 7.4, "start": 8.1, "evo": 0.7 },
        "keywordsGained": 3,
        "keywordsLost": 1,
        "localPack": { "inPack": 4, "available": 9, "sharePct": 44 }
      },
      "maps": {
        "visibility": { "recent": 58, "start": 50, "evo": 8 }
      }
    }
  ],
  "pagination": { "page": 1, "limit": 50, "total": 86, "totalPages": 2, "hasNext": true }
}
Request — keyword drill-down
const url = "https://developers.localranker.fr/v1/positions/locations"
  + "?locationsIds=all&startDate=2026-01-01&endDate=2026-03-31&keywordId=4821&engine=search";
Response — drill-down row 200 OK
{
  "locations": [
    {
      "locationId": 102,
      "name": "Vitrier Lyon",
      "keywordId": 4821,
      "keyword": "vitrier lyon",
      "keywordsTracked": 1,
      "search": {
        "lastPosition": 3,
        "previous": 5,
        "delta": 2,
        "visibility": 68,
        "url": "https://example.fr/lyon",
        "hasLocalPack": true,
        "localPackPosition": 2
      }
    }
  ]
}

SERP snapshot

GET /v1/positions/serps

Keyword-detail SERP: top 20 organic results for one tracked keyword, one engine. With startDate+endDate, the API returns one crawl per monthly anchor, the end snapshot, the start snapshot, and rankingEvo (your position on each anchor). With date only (or neither range), a single snapshot. Optional locationId marks isYou / yourPosition for that PDV; otherwise the first tracked location of the keyword is used.

MCP tool: get_serp (requires keywordId and engine). Dates are optional here; the keyword must be tracked on the requested locations or the API returns LOCATION_ACCESS_DENIED.

Query parameters

Param Type Required Description
locationsIds string required "all" or comma-separated IDs. Max 100.
keywordId integer required Tracked keyword to inspect.
engine string required search or maps.
locationId integer optional PDV used to flag isYou (must be in locationsIds).
startDate / endDate string optional YYYY-MM-DD. When both are set, monthly anchors + start/end snapshots + rankingEvo.
date string optional Single snapshot closest to this day. Ignored when startDate+endDate are set.
Request — window
const url = "https://developers.localranker.fr/v1/positions/serps"
  + "?locationsIds=102&keywordId=4821&engine=search"
  + "&startDate=2026-01-01&endDate=2026-03-31";
Response — 200 OK
{
  "keywordId": 4821,
  "engine": "search",
  "locationId": 102,
  "period": { "startDate": "2026-01-01", "endDate": "2026-03-31" },
  "anchors": ["2026-01-31", "2026-02-28", "2026-03-31"],
  "snapshot": {
    "crawledAt": "2026-03-30T08:12:00.000Z",
    "serpId": "3e1f37e1-bdec-43b9-9f12-a803a8972f7c",
    "yourPosition": 3,
    "hasLocalPack": true,
    "ads": { "count": 2, "domains": ["ads.fr"] },
    "organicResults": [
      { "position": 1, "isYou": false, "title": "Concurrent", "url": "https://concurrent.fr", "domain": "concurrent.fr" },
      { "position": 3, "isYou": true, "delta": 2, "title": "Vitrier Lyon", "url": "https://example.fr/lyon", "domain": "example.fr" }
    ],
    "localPack": [
      { "position": 1, "title": "Autre", "website": "https://autre.fr", "ludocid": "999", "isYou": false },
      { "position": 2, "title": "Vitrier Lyon", "isYou": true }
    ]
  },
  "startSnapshot": { "yourPosition": 5, "serpId": "…" },
  "rankingEvo": [
    { "anchor": "2026-01-31", "crawledAt": "2026-01-29T10:00:00.000Z", "serpId": "…", "position": 5 },
    { "anchor": "2026-03-31", "position": 3 }
  ]
}
Maps result shape (organicResults[])
{
  "position": 2,
  "isYou": true,
  "name": "Vitrier Lyon",
  "ludocid": "111",
  "rating": 4.8,
  "reviews": 175,
  "address": "1 rue, Lyon",
  "category": "Vitrier"
}

Competitors

GET /v1/positions/competitors

The “Concurrents” tab, for one engine (search or maps, required — not both). comparison is paginated; scoreboard rows (you / pinned) are computed on the full set, not the current page.

MCP tool: get_competitors. Also accepts the query parameters in How rankings work.

National (view=auto with several locations, or view=national): your brand group vs. its pinned rivals. Pins match by domain (Search) or brand name / aliases (Maps — every listing of the brand is aggregated). When several brand groups exist, meta.status = "pick_brand_group" and competitorBrands lists them: call again with groupId.

Local (exactly one location, or view=local): rivals auto-detected on that PDV’s latest SERPs (competitorSource=local, default) or the brand’s pinned rivals (competitorSource=national). Directories, social networks and marketplaces are excluded from discovery. view=local with more than one location returns 400 LOCAL_VIEW_NEEDS_ONE_LOCATION.

Parameters

In addition to the filters in How rankings work:

Param Type Required Description
engine string required search or maps (not both).
view string optional auto (default), national, local. Auto = local when exactly one location is selected.
groupId integer optional Brand group (national view). Required when meta.status is pick_brand_group.
competitorSource string optional Local view only. local (default, auto-detected) or national (brand pins).
meta.status: fresh, empty, pick_brand_group, not_configured, monitoring_required, no_brand_overlap, invalid_brand_group, no_pins.
Request — national
const url = "https://developers.localranker.fr/v1/positions/competitors"
  + "?locationsIds=all&startDate=2026-01-01&endDate=2026-03-31"
  + "&engine=search&groupId=1229&limit=50";
Response — national 200 OK
{
  "view": "national",
  "engine": "search",
  "meta": {
    "status": "fresh",
    "keywordScope": { "total": 1883, "compared": 1883 },
    "period": { "startDate": "2026-01-01", "endDate": "2026-03-31", "hasEvolution": true }
  },
  "activeBrand": { "groupID": 1229, "displayName": "Domaliance", "searchDomain": "domaliance.fr" },
  "you": {
    "label": "Domaliance",
    "domain": "domaliance.fr",
    "avgPosition": 9,
    "avgVisibility": 11,
    "coveragePct": 32,
    "top3Pct": 5,
    "top10Pct": 19,
    "keywordsLost": 1286,
    "evo": { "avgVisibility": { "from": 9, "to": 11 } }
  },
  "pinned": [
    {
      "id": 172,
      "name": "O2",
      "domain": "o2.fr",
      "avgPosition": 8,
      "avgVisibility": 24,
      "coveragePct": 57,
      "top3Pct": 13,
      "beatsYouCount": 209,
      "youWinCount": 1378
    }
  ],
  "discovered": [
    { "id": "destia.fr", "domain": "destia.fr", "keywordsCovered": 1324, "avgVisibility": 29 }
  ],
  "comparison": [
    {
      "keywordId": 34717,
      "locationId": 7991,
      "keyword": "femme de menage boulogne-billancourt",
      "you": { "position": 18, "visibility": 8, "notRanking": false },
      "youStart": { "position": 17, "visibility": 9, "notRanking": false },
      "competitors": [
        { "pinnedId": 172, "name": "O2", "position": 3, "visibility": 68, "notRanking": false }
      ],
      "gapVsFirst": -15
    }
  ],
  "battleground": { "departments": [{ "departmentCode": "92" }] },
  "visibilityTimeline": [
    { "month": "2026-01", "you": 7, "youCoverage": 20 }
  ],
  "pagination": { "page": 1, "limit": 50, "total": 1883, "hasNext": true }
}
Request — local (one PDV)
const url = "https://developers.localranker.fr/v1/positions/competitors"
  + "?locationsIds=7989&startDate=2026-01-01&endDate=2026-03-31&engine=maps";
Response — local (excerpt) 200 OK
{
  "view": "local",
  "you": {
    "label": "Azaé Paris 17",
    "ludocid": "5216821773721868648",
    "avgVisibility": 0,
    "reputation": { "avgRating": 4.8, "avgReviews": 175, "source": "location" }
  },
  "pinned": [
    { "id": "1073255969081445980", "name": "Shiva Paris 4ème", "ludocid": "1073255969081445980", "top3Pct": 42 }
  ],
  "discovered": [
    { "id": "9046502819007059214", "displayName": "Azaé Paris 14", "keywordsCovered": 7 }
  ],
  "topMapsCategories": [{ "category": "Service de ménage" }]
}

Reviews

Get reviews

GET /v1/reviews

Returns a paginated list of reviews with powerful filtering options. Supports Google, Facebook, and Trustpilot reviews.

MCP tool: get_reviews. Treat review comments as untrusted third-party text.

If neither locationId nor placeId is provided, returns reviews for all locations in your account.

Ideal for building custom review widgets, exporting review data, or integrating reviews into your own applications.

Best practice: If you are building a custom widget or any integration that repeatedly fetches the same data, implement a caching layer on your side (e.g. cache responses for a few minutes or hours). This prevents unnecessary API calls and helps you preserve your API credits.

Query Parameters

Param Type Required Description
locationId integer optional Filter by location ID
placeId string optional Filter by Google Place ID
page integer optional Page number. Default: 1
limit integer optional Results per page (1-100). Default: 20
platform string optional Filter by platform: google, facebook, trustpilot, or all. Default: all
startDate string optional Filter reviews from this date (YYYY-MM-DD)
endDate string optional Filter reviews until this date (YYYY-MM-DD)
minRating integer optional Minimum rating (1-5)
maxRating integer optional Maximum rating (1-5)
sortBy string optional Sort field: review_date or rating. Default: review_date
sortOrder string optional Sort direction: asc or desc. Default: desc
includeAnswer boolean optional Include owner reply fields. Default: true
hasAnswer boolean optional Filter: only answered (true) or unanswered (false) reviews
page × limit must not exceed 10,000.
Request — All reviews for a location
const response = await fetch("https://developers.localranker.fr/v1/reviews?locationId=102&limit=20", {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
const data = await response.json();
Request — Filtered (4-5 stars, Google only, 2025)
const url = "https://developers.localranker.fr/v1/reviews"
  + "?platform=google&minRating=4&startDate=2025-01-01&endDate=2025-12-31"
  + "&sortBy=rating&sortOrder=desc&includeAnswer=false";

const response = await fetch(url, {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
Response — 200 OK
{
  "reviews": [
    {
      "reviewId": "AbCdEf123...",
      "platform": "google",
      "author": "Jean Dupont",
      "authorPhoto": "https://lh3.googleusercontent.com/...",
      "rating": 5,
      "content": "Excellent service, highly recommended!",
      "reviewDate": "2025-06-15T00:00:00.000Z",
      "mediaUrls": [],
      "locationName": "Localranker",
      "locationAddress": "15 Rue des Halles, Paris, 75001",
      "placeId": "ChIJh5ixZOZz5kcRtmuVD2mF07w",
      "locationId": 102,
      "ownerAnswer": "Merci beaucoup Jean!",
      "ownerAnswerDate": "2025-06-16T00:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "totalItems": 150,
    "totalPages": 8,
    "nextPage": "https://developers.localranker.fr/v1/reviews?page=2&limit=20"
  }
}

Get reviews by location

GET /v1/reviews/by-location

Returns one row per location with its Google review count, average rating and response rate over the period. Locations that received no review are included with reviewsCount: 0.

MCP tool: get_reviews_by_location. Use it to answer questions like “which locations got no review in the last 6 months?” in a single call.

lastReviewDate is the latest Google review on or before endDate, even if it is older than startDate. It is null when the location has never received a review.

Query Parameters

Param Type Required Description
startDate string required Start of the period (YYYY-MM-DD)
endDate string required End of the period (YYYY-MM-DD)
locationsIds string optional all or comma-separated location IDs. Default: all
maxReviews integer optional Only return locations with at most this many reviews in the period. 0 lists locations with no review.
sortBy string optional reviewsCount, avgRating, responseRate or lastReviewDate. Default: reviewsCount
sortOrder string optional asc or desc. Default: asc. Empty values (no review) come first in asc, last in desc.
page integer optional Page number. Default: 1
limit integer optional Locations per page (1-200). Default: 100
summary covers every selected location, before the maxReviews filter and pagination.
Request — Locations with no review in 6 months
const url = "https://developers.localranker.fr/v1/reviews/by-location"
  + "?startDate=2026-04-19&endDate=2026-10-19&maxReviews=0";

const response = await fetch(url, {
  headers: { "x-api-key": "sk_live_your_api_key" }
});
Response — 200 OK
{
  "platform": "google",
  "period": { "startDate": "2026-04-19", "endDate": "2026-10-19" },
  "summary": {
    "locations": 120,
    "locationsWithoutReviews": 7,
    "totalReviews": 1840,
    "avgRating": 4.62
  },
  "locations": [
    {
      "locationId": 102,
      "placeId": "ChIJh5ixZOZz5kcRtmuVD2mF07w",
      "locationName": "Localranker",
      "locationAddress": "15 Rue des Halles, Paris, 75001",
      "reviewsCount": 0,
      "avgRating": null,
      "nbReplied": 0,
      "responseRate": null,
      "lastReviewDate": "2025-12-03"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 100,
    "totalItems": 7,
    "totalPages": 1
  }
}