Localranker API
Welcome to the Localranker API. You can use our API to access all available endpoints for managing locations, reviews, publications, and statistics.
The API is organized around REST. All requests should be made over SSL. All request and response bodies, including errors, are encoded in JSON.
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
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.
// Include in every request headers: { "x-api-key": "sk_live_your_api_key" }
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 request consumes 1 API credit. When credits are
exhausted, the API returns 429 Too Many Requests.
|
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 | Location could not be created |
{
"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 */ ]
}
{
"success": false,
"error": "No api key provided",
"code": "MISSING_API_KEY"
}
{
"success": false,
"error": "API credit exceeded",
"code": "API_CREDITS_EXCEEDED"
}
{
"success": false,
"error": "Too many requests from this IP. Max: 100/min - Please try again after 1 minute.",
"code": "RATE_LIMIT_EXCEEDED"
}
{
"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
Returns a paginated list of all locations (placeID, locationID, name, address) from your account. Results are paginated by 100.
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. |
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();
{
"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
Returns opening hours for a location. Mirrors Google Business Profile Opening Hours combined with GBP Special 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 |
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();
{
"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
Returns detailed information about a single location including title, opening hours, categories, services, address, and more.
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
locationId |
integer | optional* | Location ID |
placeId |
string | optional* | Google Place ID |
locationId or placeId must be
provided, but not both.
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();
{
"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..."
}
}
Create location
Creates a new location on Google Business Profile. Only approved accounts can use this endpoint.
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 | Create on Google Business Profile |
isTestingMode |
boolean | required | If true, validates format only without creating. 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.
|
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) |
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.
|
serviceArea.businessType: "CUSTOMER_LOCATION_ONLY" with
a serviceArea.regionCode instead.
isTestingMode: true first to validate your request
format before creating a real location.
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" } } } }) });
{
"success": true,
"data": {
"message": "TEST MODE - Location created successfully",
"requestId": "d08c141b-15d3-44ad-...",
"gbpAccountId": "123456789"
}
}
Upload 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 |
COVER, PROFILE,
LOGO, EXTERIOR, INTERIOR,
PRODUCT, AT_WORK,
FOOD_AND_DRINK, MENU,
COMMON_AREA, ROOMS, TEAMS,
ADDITIONAL
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" } ] }) });
{
"success": true,
"message": "All photos uploaded successfully",
"results": [
{ "url": "https://example.com/photo1.jpg", "status": "success" },
{ "url": "https://example.com/photo2.jpg", "status": "success" }
]
}
// See Errors — MEDIA_UPLOAD_FAILED; message + results preserved
Update 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.
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. |
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.
|
regularHours |
object | Regular opening hours (periods array) |
specialHours |
object |
Special / exceptional hours (specialHourPeriods
array)
|
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). |
storefrontAddress (regionCode is required)
and let Google geocode it. Do not send
latlng — the map pin can only be set by Google-approved
clients and is otherwise ignored. Always dry-run first with
isTestingMode: true. If Google returns
PIN_DROP_REQUIRED, the address could not be geocoded
precisely: complete/correct it, or drop the pin manually in the
Google Business Profile UI, then retry.
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
|
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" } }) });
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, // set false to apply once the dry-run passes isTestingMode: true, location: { storefrontAddress: { regionCode: "FR", addressLines: ["20 rue de Rivoli"], locality: "Paris", postalCode: "75004" } } }) });
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" } ] } }) });
{
"message": "Request received and is processing",
"requestId": 42
}
Publications
Get publications
Returns the last 10 Google Posts for a given location.
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
locationId |
integer | required | Location ID |
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();
{
"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
Returns review and performance statistics for your locations over a date range, including review counts, average ratings, response rates, and interval breakdowns.
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
|
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();
{
"reviews": {
"reviews_count": 87,
"nbReplied": 72,
"responseRate": 83,
"averageRate": 4.52
},
"report": { /* GBP performance stats */ }
}
Get GBP search 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.
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)
|
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();
{
"groupBy": "account",
"searchKeywords": {
"topKeywords": [
{ "keyword": "plombier paris", "impressions": 842, "isThreshold": false }
],
"totalImpressions": 12500,
"uniqueKeywords": 87,
"monthlyTotals": [
{ "month": "2025-01", "impressions": 4100 }
]
}
}
{
"groupBy": "location",
"locations": [
{
"locationId": 102,
"searchKeywords": { /* same shape as account.searchKeywords */ }
}
]
}
Local Audit
Get 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.
audited: false and empty issue lists.
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).
|
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();
{
"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 }
]
}
]
}
Reviews
Get reviews
Returns a paginated list of reviews with powerful filtering options. Supports Google, Facebook, and Trustpilot reviews.
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.
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.
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();
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" } });
{
"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"
}
}