API Reference
EmbedMyReviews API
Two complementary interfaces, one platform. The REST API covers structured automations: reviews, organisations, review-request invites, Google Business Profile performance data, agency-only Local Search Grid scans, and real-time webhook notifications. The AI Hub (MCP) covers interactive AI work: every read and write surface exposed as tools any MCP-compatible client like Claude Desktop, Cursor, or ChatGPT can call over JSON-RPC 2.0.
https://app.your-white-label.com/api/v1Authentication
All API requests require a valid API token. Create tokens from your account settings under API & Webhooks. Include it in the Authorization header of every request.
Authorization Header
Authorization: Bearer your-api-tokenToken permissions
| Permission | Access |
|---|---|
| reviews:read | View reviews and review sources |
| reviews:create | Create reviews |
| reviews:update | Update reviews |
| reviews:delete | Delete reviews |
| request:read | View campaigns |
| request:create | Send review invites |
| organizations:read | View organisations |
| organizations:create | Create organisations |
| organizations:update | Update organisations |
| organizations:delete | Delete organisations |
Rate limiting
60 requests per minute per token. Every response includes rate limit headers.
| Header | Description |
|---|---|
| X-RateLimit-Limit | Maximum requests per window (60) |
| X-RateLimit-Remaining | Remaining requests in current window |
| Retry-After | Seconds to wait before retrying (only on 429) |
Errors
Standard HTTP status codes. Errors return JSON with a message field.
| Code | Status | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource successfully created |
| 202 | Accepted | Request accepted and queued |
| 401 | Unauthenticated | Missing or invalid API token |
| 403 | Forbidden | Token lacks required permissions |
| 404 | Not Found | Resource does not exist |
| 422 | Validation Error | Request body failed validation |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Server Error | Something went wrong on our end |
Pagination
List endpoints return paginated results with 10 items per page. Use the page query parameter to navigate.
{
"data": [...],
"links": {
"first": "https://app.your-white-label.com/api/v1/reviews?page=1",
"last": "https://app.your-white-label.com/api/v1/reviews?page=5",
"prev": null,
"next": "https://app.your-white-label.com/api/v1/reviews?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 10,
"to": 10,
"total": 48
}
}Validation error (422) response
{
"message": "The email field is required when phone is not present.",
"errors": {
"email": [
"The email field is required when phone is not present."
]
}
}/api/v1/reviewsList reviews
Retrieve a paginated list of reviews across all connected sources. Filter by rating, organisation, location, or review source.
Requires reviews:read
Query parameters
| Parameter | Type | Description |
|---|---|---|
| rating | integer | Filter by rating (1-5) |
| organization_id | integer | Filter by organisation |
| location_id | integer | Filter by location |
| source_names[] | array | Filter by source names (e.g. Google, Facebook) |
| page | integer | Page number (default: 1) |
Example request
curl https://app.your-white-label.com/api/v1/reviews?rating=5 \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"data": [
{
"id": 142,
"organization_id": 1,
"location_id": 3,
"integration_id": 12,
"author": "Sarah Johnson",
"date": "2025-12-15T10:30:00.000000Z",
"rating": 5,
"title": null,
"message": "Outstanding service!",
"source": "Google",
"source_logo": "https://example.com/logos/google.svg",
"avatar": "https://...",
"reply": "Thank you Sarah!",
"reply_date": "2025-12-16T09:00:00.000000Z",
"hidden": false,
"verified": false
}
],
"links": { "first": "...", "last": "...", "prev": null, "next": "..." },
"meta": { "current_page": 1, "from": 1, "last_page": 5, "per_page": 10, "to": 10, "total": 48 }
}/api/v1/reviews/{id}Get review
Retrieve a single review by its ID.
Requires reviews:read
Example request
curl https://app.your-white-label.com/api/v1/reviews/142 \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"data": {
"id": 142,
"organization_id": 1,
"location_id": 3,
"integration_id": 42,
"author": "Sarah Johnson",
"date": "2025-12-15T10:30:00.000000Z",
"rating": 5,
"title": null,
"message": "Outstanding service!",
"source": "Testimonials",
"source_logo": "https://cdn.revw.me/img/integration-logos/testimonials.svg",
"avatar": "https://...",
"reply": null,
"reply_date": null,
"hidden": false,
"verified": false
}
}/api/v1/reviewsCreate review
Create a new review from a custom or testimonial source. Only custom integrations and testimonials are supported, so reviews from synced sources like Google or Facebook cannot be created via the API. Use the Writable Sources endpoint to discover available sources.
Requires reviews:read + reviews:create
Request body
| Parameter | Type | Description |
|---|---|---|
| author | string | Reviewer name (required, max 255 chars) |
| rating | integer | Star rating 1-5 (required) |
| published_on | date | Publication date in Y-m-d format (required) |
| organization_id | integer | Organisation ID (required) |
| location_id | integer | Location ID, must belong to the organisation (required) |
| source | integer | Integration ID from the Writable Sources endpoint (required) |
| message | string | Review content |
| title | string | Review title (max 255 chars) |
| avatar | file | Reviewer avatar (png, jpg, jpeg, svg, max 250KB) |
Example request
curl -X POST https://app.your-white-label.com/api/v1/reviews \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json" \ -F "author=Jane Smith" \ -F "rating=5" \ -F "message=Absolutely fantastic experience!" \ -F "published_on=2026-01-15" \ -F "organization_id=1" \ -F "location_id=3" \ -F "source=42"
Example response 201 Created
{
"data": {
"id": 285,
"organization_id": 1,
"location_id": 3,
"integration_id": 42,
"author": "Jane Smith",
"date": "2026-01-15T00:00:00.000000Z",
"rating": 5,
"title": null,
"message": "Absolutely fantastic experience!",
"source": "Testimonials",
"source_logo": "https://cdn.revw.me/img/integration-logos/testimonials.svg",
"avatar": "https://ui-avatars.com/api/?name=Jane Smith&background=333D9B&size=74&color=ffffff&rounded=1",
"reply": null,
"reply_date": null,
"hidden": false,
"verified": false
}
}Important
Only custom and testimonial sources are supported. Use GET /api/v1/reviews/sources to discover available sources and their IDs.
The location must belong to the specified organisation, and your token must have access to both.
/api/v1/reviews/{id}Update review
Update an existing review. All fields are optional, so only include the fields you want to change. Only reviews from custom or testimonial sources can be updated. Source cannot be changed.
Requires reviews:read + reviews:update
Request body
| Parameter | Type | Description |
|---|---|---|
| author | string | Reviewer name (max 255 chars) |
| rating | integer | Star rating 1-5 |
| message | string | Review content |
| title | string | Review title (max 255 chars) |
| published_on | date | Publication date in Y-m-d format |
| avatar | file | Reviewer avatar (png, jpg, jpeg, svg, max 250KB) |
Example request
curl -X PUT https://app.your-white-label.com/api/v1/reviews/285 \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json" \ -F "rating=4" \ -F "message=Updated review content."
Important
Only reviews from custom integrations and testimonial sources can be updated. Synced reviews (Google, Facebook, etc.) return 403.
Source cannot be changed. To change source, delete and recreate the review.
/api/v1/reviews/{id}Delete review
Permanently delete a review. This action cannot be undone. Only reviews from custom or testimonial sources can be deleted.
Requires reviews:read + reviews:delete
Example request
curl -X DELETE https://app.your-white-label.com/api/v1/reviews/285 \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"message": "Review deleted successfully."
}Important
Only reviews from custom integrations and testimonial sources can be deleted. Synced reviews (Google, Facebook, etc.) return 403.
/api/v1/reviews/sourcesWritable review sources
Returns sources available for creating reviews (custom integrations and testimonials). The id from each source is what you pass as the source parameter when creating a review.
Requires reviews:read
Query parameters
| Parameter | Type | Description |
|---|---|---|
| organization_id | integer | Filter by organisation |
| location_id | integer | Filter by location |
| page | integer | Page number (default: 1) |
Example request
curl https://app.your-white-label.com/api/v1/reviews/sources?location_id=3 \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"data": [
{
"id": 42,
"name": "Testimonials",
"logo": "https://cdn.revw.me/img/integration-logos/testimonials.svg",
"type": "testimonials",
"organization_id": 1,
"location_id": 3
},
{
"id": 58,
"name": "My Custom Source",
"logo": "https://cdn.revw.me/...",
"type": "custom",
"organization_id": 1,
"location_id": 3
}
],
"links": { ... },
"meta": { ... }
}/api/v1/organizationsList organisations
Returns a paginated list of all organisations accessible to the authenticated user.
Requires organizations:read
Example request
curl https://app.your-white-label.com/api/v1/organizations \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"data": [
{
"id": 1,
"name": "Acme Dental Clinic",
"created_at": "2025-01-15T08:30:00.000000Z",
"updated_at": "2025-06-20T14:22:00.000000Z"
}
],
"links": { ... },
"meta": { ... }
}/api/v1/organizationsCreate organisation
Create a new organisation. The name must be unique across your account.
Requires organizations:create
Request body
| Parameter | Type | Description |
|---|---|---|
| name | string | Organisation name (required, max 255 chars, unique) |
| logo | file | Logo image (png, jpg, jpeg, svg, max 250KB) |
Example request
curl -X POST https://app.your-white-label.com/api/v1/organizations \
-H "Authorization: Bearer your-api-token" \
-H "Content-Type: application/json" \
-d '{"name": "Downtown Branch"}'Example response 201 Created
{
"id": 2,
"name": "Downtown Branch",
"created_at": "2025-12-15T10:30:00.000000Z",
"updated_at": "2025-12-15T10:30:00.000000Z"
}/api/v1/organizations/{id}Get organisation
Retrieve a single organisation by its ID.
Requires organizations:read
Example request
curl https://app.your-white-label.com/api/v1/organizations/1 \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"id": 1,
"name": "Acme Dental Clinic",
"created_at": "2025-01-15T08:30:00.000000Z",
"updated_at": "2025-06-20T14:22:00.000000Z"
}/api/v1/organizations/{id}Update organisation
Update an existing organisation's name or logo.
Requires organizations:update
Request body
| Parameter | Type | Description |
|---|---|---|
| name | string | Organisation name (max 255 chars, unique) |
| logo | file | Logo image (png, jpg, jpeg, svg, max 250KB) |
Example request
curl -X PUT https://app.your-white-label.com/api/v1/organizations/1 \
-H "Authorization: Bearer your-api-token" \
-H "Content-Type: application/json" \
-d '{"name": "Updated Name"}'/api/v1/organizations/{id}Delete organisation
Permanently delete an organisation. This action cannot be undone. You must have at least one remaining organisation.
Requires organizations:delete
Example request
curl -X DELETE https://app.your-white-label.com/api/v1/organizations/1 \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"message": "Organization deleted successfully."
}/api/v1/request-reviews/campaignsList campaigns
Retrieve your review request campaigns. Use campaign UUIDs to send invites via the Send Invite endpoint.
Requires request:read
Query parameters
| Parameter | Type | Description |
|---|---|---|
| organization_id | integer | Filter campaigns by organisation |
| search | string | Search campaigns by name |
Example request
curl https://app.your-white-label.com/api/v1/request-reviews/campaigns \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Post-Visit Follow Up"
},
{
"id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
"name": "Monthly Customer Outreach"
}
],
"links": { ... },
"meta": { ... }
}/api/v1/request-reviews/campaigns/{campaign}/inviteSend review invite
Send a review request invite to a contact via email, SMS, or WhatsApp. This is the primary endpoint for automating review requests from your CRM, POS, or booking system.
Requires request:read + request:create
Path + request body
| Parameter | Type | Description |
|---|---|---|
| campaign | uuid | Campaign UUID (required path parameter, from List Campaigns) |
| string | Contact email. Required when phone is not provided. | |
| phone | string | Phone in E.164 format (e.g. +14275238194). Required when email is not provided. |
| first_name | string | Contact first name (max 255 chars) |
| last_name | string | Contact last name (max 255 chars) |
| send_after_hours | integer | Hours to wait before sending. 0 = send ASAP. null = use campaign default. |
Example request
curl -X POST https://app.your-white-label.com/api/v1/request-reviews/campaigns/{campaign_uuid}/invite \
-H "Authorization: Bearer your-api-token" \
-H "Content-Type: application/json" \
-d '{
"email": "sarah@example.com",
"first_name": "Sarah",
"send_after_hours": 0
}'Example response 202 Accepted
{
"id": "c7d8e9f0-1234-5678-abcd-ef9876543210",
"message": "Invites sent successfully."
}Important
Duplicate contacts are handled gracefully. If the same contact is submitted again within the campaign's dedup window (default: 14 days), the request returns 202 without error and the contact is silently ignored.
Unsubscribed contacts will not receive new invites, even via the API. Their opt-out is always respected.
Invites respect the campaign's configured send window. Requests outside the window are queued for the next available slot.
Delivery channels (email, SMS, WhatsApp) depend on your campaign step configuration. Providing both email and phone maximises reach.
/api/v1/sourcesList review sources
Retrieve all available review sources (Google, Facebook, Yelp, Trustpilot, etc.) with their capabilities and logos.
Requires Any valid token
Example request
curl https://app.your-white-label.com/api/v1/sources \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"data": [
{
"id": 1,
"name": "Google",
"url": "https://google.com",
"logo": "https://...",
"available": true,
"can_collect_reviews": true,
"deep_linking_supported": true
}
]
}/api/v1/countriesList countries
Retrieve the full list of supported countries with ISO 3166-1 alpha-2 codes. Useful for populating country selectors.
Requires Any valid token
Example response 200 OK
[
{ "id": "US", "name": "United States" },
{ "id": "GB", "name": "United Kingdom" },
{ "id": "CA", "name": "Canada" },
...
]/api/v1/meCurrent user
Retrieve the authenticated user's basic information. Useful for verifying your API token.
Requires Any valid token
Example response 200 OK
{
"id": 1,
"name": "John Smith",
"email": "john@example.com"
}/api/v1/gbp/metricsGoogle Business Profile metrics
Aggregated impressions (search, maps, desktop, mobile), call clicks, website clicks, and direction requests for one location over a date range. The response also includes the totals for the equal-length window immediately before the requested period, so you can compute trend deltas without a second call.
Requires reviews:read
Query parameters
| Parameter | Type | Description |
|---|---|---|
| location_id | integer | Internal location ID (required, from /api/v1/locations). |
| from | date | Inclusive start date in Y-m-d format. Defaults to 30 days ago. |
| to | date | Inclusive end date in Y-m-d format. Defaults to yesterday. |
| include_daily | boolean | Include the per-day rows in the response. Default false. |
Example request
curl "https://app.your-white-label.com/api/v1/gbp/metrics?location_id=123&from=2026-04-01&to=2026-04-30" \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"data": {
"location_id": 123,
"organization_id": 45,
"period": { "from": "2026-04-01", "to": "2026-04-30" },
"totals": {
"impressions_total": 21110,
"impressions_search": 12345,
"impressions_maps": 8765,
"impressions_desktop": 6543,
"impressions_mobile": 14567,
"call_clicks": 89,
"website_clicks": 234,
"direction_requests": 156
},
"previous_period": {
"from": "2026-03-02",
"to": "2026-03-31",
"totals": { "impressions_total": 17800 }
}
}
}Important
Each token only sees the locations its user can open in the dashboard. A team member with restricted role gets 403 location_access_denied for locations outside their scope.
Profile views and Photo views are no longer available from the Google Performance API and are not returned, even when present in older data. Impressions, calls, direction requests, website clicks, and search query terms are still available.
Maximum date range is 540 days. Wider windows return 422 invalid_date_range so you can chunk the request.
/api/v1/gbp/search-termsGBP search query terms
The search query terms shoppers used to find this location, with their impression counts, ordered by impressions descending. Below-threshold rows (Google masks impression counts for very low volume terms) are returned with below_threshold=true and sorted to the bottom of the list.
Requires reviews:read
Query parameters
| Parameter | Type | Description |
|---|---|---|
| location_id | integer | Internal location ID (required). |
| year | integer | Defaults to the current year. |
| month | integer | 1-12. Omit to return every month in the year. |
| per_page | integer | Default 50, max 200. |
| page | integer | 1-based page number. |
Example request
curl "https://app.your-white-label.com/api/v1/gbp/search-terms?location_id=123&year=2026&month=4" \ -H "Authorization: Bearer your-api-token" \ -H "Accept: application/json"
Example response 200 OK
{
"data": [
{
"keyword": "emergency dentist near me",
"year": 2026,
"month": 4,
"month_label": "Apr 2026",
"impressions": 234,
"below_threshold": false,
"location_id": 123,
"organization_id": 45
}
],
"meta": {
"current_page": 1,
"per_page": 50,
"total": 87,
"location_id": 123,
"organization_id": 45,
"year": 2026,
"month": 4
}
}Important
Same per-token, per-location access rules as the metrics endpoint. A 403 location_access_denied means the caller cannot see this location.
Below-threshold rows still appear in the list so you can show "Google masked this keyword" instead of leaving a gap in the report.
Agency API
The Agency API lets you manage your customers programmatically. Create accounts, switch plans, adjust limits, manage credits, handle NFC orders, and pull Local Search Grid data. These endpoints use the same authentication but require an agency-level API token generated from the agency account. None of the Local Search Grid endpoints below are reachable by your customers, even if they hold their own API token.
Base URL
https://app.your-white-label.com/api/agency/v1Agency tokens have full access across all customer accounts. There are no scoped permissions here, so the token acts as a super admin.
/api/agency/v1/customersCreate customer
Create a new customer account. Optionally email them their login details and a secure one-click link to connect their Google Business Profile and/or Facebook Page without ever logging into the platform.
Request body
| Parameter | Type | Description |
|---|---|---|
| name | string | Customer name (required) |
| company | string | Organisation/company name (required) |
| string | Login email address (required) | |
| password | string | Password, min 8 characters (required) |
| custom_plan_id | string | Plan ID to assign (required) |
| phone | string | Phone in E.164 format (optional) |
| location_limit | integer | Override plan location limit (optional, min 1) |
| member_limit | integer | Override plan member limit. 0 = unlimited (optional) |
| send_invite | boolean | Email login details to the customer (optional) |
| send_google_connect_link | boolean | Email a secure one-click link to connect their Google Business Profile. The customer signs in directly with Google, no platform login needed. The link is bound to their auto-created default location and is valid for 7 days (optional). |
| send_facebook_connect_link | boolean | Same as send_google_connect_link but for Facebook Pages. Independent of the Google flag: send one, the other, both, or neither (optional). |
| country | string | ISO 3166-1 alpha-2 code. Required if Stripe Tax is enabled. |
| postal_code | string | Required for US/CA when Stripe Tax is enabled |
Example request
curl -X POST https://app.your-white-label.com/api/agency/v1/customers \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"name": "John Smith",
"company": "My Clinic",
"email": "john@example.com",
"password": "Password123456",
"custom_plan_id": "2",
"send_invite": true,
"send_google_connect_link": true,
"send_facebook_connect_link": false
}'Example response 200 OK
{
"data": {
"id": 1,
"name": "John Smith",
"email": "john@example.com",
"phone": null,
"language": "en",
"trial_ends_at": null,
"created_at": "2024-09-26T19:57:52.000000Z",
"updated_at": "2024-09-26T19:57:52.000000Z",
"custom_plan": { "id": 2, "name": "Pro" },
"organizations": [{ "id": 1, "name": "My Clinic" }]
}
}Notes
- send_google_connect_link and send_facebook_connect_link are independent. Set neither, one, or both.
- Each connect link email goes through your configured transactional email provider (the same provider used for invites and other transactional sends) and appears in your transactional mail log.
- A delivery failure on either provider is logged but does not block the customer record from being created. The customer is always returned with 200/201.
- When the customer clicks the link, they sign in directly with Google or Facebook. They never see a platform login screen. The integration is auto-attached to their default location.
- You can revoke or re-issue a connect link at any time from the customer's integration manager in the UI.
/api/agency/v1/customers/{customer}Switch customer plan
Change the customer's assigned plan.
Request body
| Parameter | Type | Description |
|---|---|---|
| custom_plan_id | string | The plan ID to switch to (required) |
Example request
curl -X PUT https://app.your-white-label.com/api/agency/v1/customers/{customer} \
-H "Authorization: Bearer {token}" \
-d custom_plan_id="2"Example response 200 OK
{
"data": {
"id": 1,
"name": "John Smith",
"email": "john@example.com",
"custom_plan": { "id": 2, "name": "Pro" },
"organizations": [{ "id": 1, "name": "My Clinic" }]
}
}/api/agency/v1/customers/{customer}/location-limitUpdate location & member limits
Override the customer's location or member limit independently of their plan defaults. Two separate endpoints.
Request body
| Parameter | Type | Description |
|---|---|---|
| location_limit | integer | PUT .../location-limit: number of locations allowed (min 1) |
| member_limit | integer | PUT .../member-limit: number of team members allowed (0 = unlimited) |
Example request
# Update location limit
curl -X PUT https://app.your-white-label.com/api/agency/v1/customers/{customer}/location-limit \
-H "Authorization: Bearer {token}" \
-d location_limit="5"
# Update member limit
curl -X PUT https://app.your-white-label.com/api/agency/v1/customers/{customer}/member-limit \
-H "Authorization: Bearer {token}" \
-d member_limit="10"Example response 200 OK
{ "message": "Location limit updated successfully." }
// or
{ "message": "Member limit updated successfully." }/api/agency/v1/customers/{customer}/add-creditsAdd credits
Add email or SMS credits to a customer's account. Credits can optionally expire.
Request body
| Parameter | Type | Description |
|---|---|---|
| type | string | Credit type: "email" or "sms" (required) |
| credits | integer | Number of credits to add, min 1 (required) |
| expires_at | date | Expiry date in Y-m-d format (optional) |
| notes | string | Note visible to the customer (optional) |
Example request
curl -X POST https://app.your-white-label.com/api/agency/v1/customers/{customer}/add-credits \
-H "Authorization: Bearer {token}" \
-d type="email" \
-d credits="1000"Example response 200 OK
{
"message": "Credits added successfully.",
"balance": 250
}/api/agency/v1/customers/{customer}/pausePause & resume subscription
Pause or resume a customer's subscription. Only available when the plan is not linked to Stripe (for Stripe-managed subscriptions, use Stripe's native pause).
Example request
# Pause subscription
curl -X PUT https://app.your-white-label.com/api/agency/v1/customers/{customer}/pause \
-H "Authorization: Bearer {token}"
# Resume subscription
curl -X PUT https://app.your-white-label.com/api/agency/v1/customers/{customer}/resume \
-H "Authorization: Bearer {token}"Example response 200 OK
{ "message": "Subscription has been paused." }
// or
{ "message": "Subscription has been resumed." }/api/agency/v1/customers/{customer}Delete customer
Permanently delete a customer account.
Example request
curl -X DELETE https://app.your-white-label.com/api/agency/v1/customers/{customer} \
-H "Authorization: Bearer {token}"Example response 200 OK
{ "message": "Customer deleted successfully." }/api/agency/v1/customersList all customers
Retrieve a paginated list of all your customers with their plans and organisations.
Parameters
| Parameter | Type | Description |
|---|---|---|
| search | string | Search by name or email (optional) |
Example request
curl https://app.your-white-label.com/api/agency/v1/customers \
-H "Authorization: Bearer {token}"Example response 200 OK
{
"data": [
{
"id": 1,
"name": "John Smith",
"email": "john@example.com",
"phone": null,
"language": "en",
"trial_ends_at": "2023-08-24T16:57:57.000000Z",
"created_at": "2023-07-25T16:57:57.000000Z",
"updated_at": "2023-08-04T15:01:11.000000Z",
"custom_plan": { "id": 2, "name": "Pro" },
"organizations": [{ "id": 2, "name": "My Clinic" }]
}
],
"links": { ... },
"meta": { "current_page": 1, "from": 1, "per_page": 10, "to": 10 }
}/api/agency/v1/custom-plansList all plans
Retrieve all custom plans you've created. Use the plan ID when creating customers or switching plans.
Example request
curl https://app.your-white-label.com/api/agency/v1/custom-plans \
-H "Authorization: Bearer {token}"Example response 200 OK
{
"data": [
{
"id": 1,
"stripe_monthly_id": "",
"stripe_yearly_id": "",
"name": "Basic",
"is_default": false,
"is_hidden": false,
"is_free": true,
"updated_at": "2024-09-26T10:19:58.000000Z",
"created_at": "2023-07-25T15:14:02.000000Z"
}
]
}/api/agency/v1/ordersOrders (NFC products)
Manage NFC product orders. List all orders, mark as shipped (with optional tracking URL), complete, or cancel.
Example request
# List orders
curl https://app.your-white-label.com/api/agency/v1/orders \
-H "Authorization: Bearer {token}"
# Ship order (with optional tracking URL)
curl -X PUT https://app.your-white-label.com/api/agency/v1/orders/{order}/ship \
-H "Authorization: Bearer {token}" \
-d tracking_url="https://tracking.example.com/123"
# Complete order
curl -X PUT https://app.your-white-label.com/api/agency/v1/orders/{order}/complete \
-H "Authorization: Bearer {token}"
# Cancel order
curl -X PUT https://app.your-white-label.com/api/agency/v1/orders/{order}/cancel \
-H "Authorization: Bearer {token}"Example response 200 OK
{
"data": [
{
"id": "27d2ba02-...",
"order_number": "HUFTMX-1733678942",
"email": "john@example.com",
"total": 49.99,
"currency": "usd",
"status": "completed",
"shipping": {
"name": "John Smith",
"address": "49 Featherstone Street",
"city": "London",
"country": "GB",
"tracking_url": null
},
"items": [
{
"product_name": "Google Review Card",
"quantity": 1,
"unit_price": 29.99,
"nfc_urls": ["https://your-domain.com/nfc/..."]
}
]
}
]
}/api/agency/v1/scansList Local Search Grid scans
Paginated metadata for every Local Search Grid scan the agency owns, newest first. Filter by schedule, completion state, recency, or free-text. Restricted to the tenant owner.
Parameters
| Parameter | Type | Description |
|---|---|---|
| search | string | Free-text over location and keyword. |
| is_scheduled | boolean | Only recurring scans. |
| completed | boolean | Only scans with a finished snapshot. |
| failed | boolean | Only scans whose last run failed. |
| since_days | integer | Limit to scans created in the last N days (1-365). |
| per_page | integer | 1-100, default 20. |
| page | integer | 1-based page number. |
Example request
curl "https://app.your-white-label.com/api/agency/v1/scans?completed=true&since_days=30" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Example response 200 OK
{
"data": [
{
"uuid": "4f3a3b6c-...",
"location": "Acme Dental, Downtown",
"keyword": "dentist near me",
"keywords": ["dentist near me", "emergency dentist"],
"grid_size": 7,
"is_scheduled": true,
"is_running": false,
"last_run_at": "2026-04-15T10:30:00+00:00",
"next_run_at": "2026-05-01T13:00:00+00:00"
}
],
"meta": { "current_page": 1, "per_page": 20, "total": 24 }
}Notes
- Customers on your platform never see these endpoints, even with their own API token. Authorization is gated on tenant ownership, not just the use_api plan option.
/api/agency/v1/scans/{uuid}Get a Local Search Grid scan
Returns one scan with its per-pin results for the requested snapshot. Defaults to the most recent snapshot; pass snapshot_at to load a historical one. Each pin's top_results list is capped at top_n (0-20, default 5) so the response stays bounded on large grids. For paginated per-pin access, use the data_points endpoint below.
Parameters
| Parameter | Type | Description |
|---|---|---|
| uuid | string | Scan UUID (required path parameter). |
| snapshot_at | datetime | ISO-8601 timestamp from available_snapshots. Defaults to the latest snapshot. |
| top_n | integer | SERP entries per pin (0-20). Default 5. Pass 20 for the full SERP list per pin. |
Example request
curl "https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-...?snapshot_at=2026-04-15T10:30:00Z&top_n=5" \
-H "Authorization: Bearer {token}"Example response 200 OK
{
"data": {
"uuid": "4f3a3b6c-...",
"location": "Acme Dental, Downtown",
"keyword": "dentist near me",
"keywords": ["dentist near me", "emergency dentist"],
"grid_size": 7,
"is_running": false,
"summary": {
"average_rank": 5.2,
"top_3_count": 12,
"top_4_to_10_count": 23,
"not_ranking_count": 14
},
"report_url": "https://app.your-white-label.com/scans/4f3a3b6c-.../report",
"available_snapshots": [
"2026-04-01T10:30:00+00:00",
"2026-04-15T10:30:00+00:00",
"2026-05-01T10:30:00+00:00"
],
"results": [
{
"lat": 40.71, "lng": -74.00, "rank": 3,
"keyword": "dentist near me", "status": "complete",
"top_results": [
{ "place_id": "ChIJ...", "name": "Acme Dental", "rank": 1 }
]
}
]
}
}Notes
- For large grids (15×15 with multi-keyword scans), prefer the data_points endpoint below. It is paginated and lean, returning only what most rank-tracker integrations need.
/api/agency/v1/scans/{uuid}/data_pointsLSG pin data (paginated)
Lean, paginated per-pin data designed for rank-tracker integrations and pin-layer heatmap renderers. Returns one row per pin with the target business's rank and the top-N competitors (place_id, name, rank). Always paginated, so the response size is bounded regardless of grid scale.
Parameters
| Parameter | Type | Description |
|---|---|---|
| uuid | string | Scan UUID (required path parameter). |
| snapshot_at | datetime | ISO-8601 timestamp. Defaults to the latest snapshot for the (scan, keyword) combo. |
| keyword | string | Restrict to one keyword on multi-keyword scans. |
| top_n | integer | Competitors per pin (0-20). Default 5. Pass 0 for just lat/lng/target_rank. |
| per_page | integer | 1-500. Default 100. |
| page | integer | 1-based. |
Example request
curl "https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-.../data_points?per_page=100&top_n=5" \
-H "Authorization: Bearer {token}"Example response 200 OK
{
"data": [
{
"lat": 39.96,
"lng": -86.28,
"keyword": "dentist near me",
"snapshot_at": "2026-04-15T10:30:00+00:00",
"target_rank": 4,
"results": [
{ "place_id": "ChIJ...", "name": "Acme Dental", "rank": 1 },
{ "place_id": "ChIJ...", "name": "Best Smiles", "rank": 2 }
]
}
],
"links": { "first": "...", "last": "...", "prev": null, "next": "..." },
"meta": {
"current_page": 1,
"per_page": 100,
"total": 1125,
"scan_uuid": "4f3a3b6c-...",
"snapshot_at": "2026-04-15T10:30:00+00:00",
"keyword": "dentist near me",
"top_n": 5
}
}Notes
- target_rank is null when the target business was not found in the top 20 at that pin (the in-app UI labels this "NR").
- Scans with no completed snapshots return 200 with data: [] and meta.total: 0 so pollers can branch on meta.total rather than HTTP status.
- A 13×13 × 6-keyword scan is 1,014 pins per snapshot. With per_page=100 that is 11 pages.
/api/agency/v1/scans/{uuid}/snapshotsSnapshot history
Aggregated rank distribution and visibility score for every snapshot the scan has produced. Each row is one snapshot. Use this to plot a long-range trend without paging through every pin.
Parameters
| Parameter | Type | Description |
|---|---|---|
| uuid | string | Scan UUID (required path parameter). |
| keyword | string | Restrict the aggregate to one keyword on a multi-keyword scan. |
Example request
curl "https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-.../snapshots?keyword=dentist+near+me" \
-H "Authorization: Bearer {token}"Example response 200 OK
{
"data": [
{
"snapshot_at": "2026-04-01T10:30:00+00:00",
"label": "1 April 2026 10:30",
"short_label": "1 Apr",
"month_year": "April 2026",
"avg_rank": 12.1,
"change_from_previous": 0.0,
"top_3_count": 3,
"top_10_count": 9,
"below_top_10_count": 8,
"not_ranking_count": 5,
"total_pins": 25,
"visibility_score": 18.6
}
]
}Notes
- change_from_previous is the delta vs the previous snapshot's avg_rank. Negative values mean the average rank improved (lower is better).
/api/agency/v1/scans/{uuid}/insightsDeep insights
Everything the in-app report computes: summary stats, long-range trends, snapshot-over-snapshot deltas, geographic quadrant strength, top competitors, and rule-based recommendations. Defaults to the latest snapshot.
Parameters
| Parameter | Type | Description |
|---|---|---|
| uuid | string | Scan UUID (required path parameter). |
| keyword | string | Restrict to one keyword on multi-keyword scans. |
| snapshot_at | datetime | ISO-8601 timestamp; default is the latest snapshot. |
Example request
curl "https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-.../insights" \
-H "Authorization: Bearer {token}"Example response 200 OK
{
"data": {
"scan_uuid": "4f3a3b6c-...",
"snapshot_at": "2026-05-01T10:30:00+00:00",
"keyword": "dentist near me",
"summary": {
"avg_rank": 7.3,
"visibility_score": 24.5,
"top_3_pins": 4,
"top_10_pins": 9,
"not_ranking": 5,
"total_pins": 25
},
"trends_vs_first_snapshot": {
"first_snapshot_at": "2026-04-01T10:30:00+00:00",
"start_avg": 12.1,
"current_avg": 7.3,
"change": -4.8,
"improved": true,
"percent_change": 39.7
},
"trends_vs_previous_snapshot": {
"previous_snapshot_at": "2026-04-15T10:30:00+00:00",
"avg_rank_change": -1.2,
"visibility_change": 3.1,
"top_3_change": 1,
"top_10_change": 2
},
"geographic_strength": [
{ "label": "North", "pins": 7, "avg_rank": 8.0, "visibility": 18.2, "coverage": 86 }
],
"top_competitors": [
{ "name": "Competitor One", "avg_rank": 3.2, "visibility": 45.6, "threat_score": 60.0 }
],
"recommendations": [
{ "type": "warning", "icon": "eye-off", "title": "Low visibility", "description": "..." }
]
}
}Notes
- Scans with no completed snapshots still return 200 with summary: null. Branch on field presence rather than HTTP status.
/api/agency/v1/scansCreate a scan
Creates a brand-new Local Search Grid scan, generates the hex-ring pin grid server-side, and queues the first snapshot immediately. Returns 202 Accepted with the new scan UUID. Subscribe to scan-completed to be notified when the snapshot finishes.
Request body
| Parameter | Type | Description |
|---|---|---|
| location | string | Human-readable name (required). Shows in the dashboard. |
| address | string | Used only if lat/lng are not supplied (server geocodes via Google Maps). |
| keywords | array<string> | 1-20 keyword strings (required). |
| grid_size | integer | One of 3, 5, 7, 9, 11, 13, 15 (required). |
| distance | number | Distance from the center to the outer ring, greater than 0, max 50 (required). |
| distance_unit | string | "km" or "miles" (required). |
| type | string | "business" (default) or "sab" for service-area businesses. |
| target_place_id | string | Google Place ID, used to score "self" pins in competitor analysis. |
| lat | number | Pin grid center latitude. |
| lng | number | Pin grid center longitude. |
Example request
curl -X POST https://app.your-white-label.com/api/agency/v1/scans \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"location": "Acme Dental, Downtown",
"keywords": ["dentist near me", "emergency dentist"],
"grid_size": 7,
"distance": 2.0,
"distance_unit": "miles",
"target_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
"lat": 42.3601,
"lng": -71.0589
}'Example response 202 Accepted
{
"data": {
"uuid": "4f3a3b6c-...",
"started_at": "2026-05-12T11:45:00+00:00",
"is_running": true
}
}Notes
- Coordinate resolution order: (target_place_id + lat + lng) is used as-is; (lat + lng) only is used as-is; address only is geocoded via your LocalSearchGridProvider's Google Maps key. If geocoding fails the call returns 422 missing_coords.
- A 422 provider_not_configured response means you have not configured DataForSEO + Google Maps credentials yet. Wire them up in Settings then retry.
- A 422 invalid_grid_params response means the grid_size and distance combination is unsupported (for example, the center landing at the geographic poles).
/api/agency/v1/scans/{uuid}/runRe-run a scan
Snaps a new snapshot using the saved grid, keywords, and distance. Returns 202 Accepted immediately and fires the scan-completed webhook (or scan-failed if the run errors) once the snapshot finishes.
Example request
curl -X POST https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-.../run \
-H "Authorization: Bearer {token}"Example response 202 Accepted
{
"data": {
"uuid": "4f3a3b6c-...",
"started_at": "2026-05-12T12:00:00+00:00",
"is_running": true
}
}Notes
- A 409 response means the scan is already running. Wait for the current snapshot to finish or subscribe to the webhook instead of polling.
- A 422 response means the scan has no provider configured, no keywords, or no saved pins. Open the scan in the dashboard to confirm.
- Bulk-create burst protection: scans 6+ in a 60-second window get a per-scan stagger on their first snapshot to keep DataForSEO from throttling. Scheduled re-runs are unaffected.
/api/agency/v1/scans/{uuid}Update a scan
Updates an existing scan without abandoning its snapshot history. All body fields are optional (PATCH-like on PUT). Metadata changes (location, address, target_place_id, type, keywords) are always allowed. Geometry changes (grid_size, distance, distance_unit, lat, lng) are allowed when the scan has no completed snapshots, or with regenerate_pins=true when it does.
Request body
| Parameter | Type | Description |
|---|---|---|
| location | string | Display name shown in the dashboard. |
| address | string | Display-only. |
| keywords | array<string> | Full replacement set, 1-20 strings, each ≤ 100 chars. |
| target_place_id | string | Google Place ID. Used to score "self" in competitor analysis. |
| type | string | "business" or "sab". |
| grid_size | integer | Geometry. One of 3, 5, 7, 9, 11, 13, 15. |
| distance | number | Geometry. > 0 and ≤ 50. |
| distance_unit | string | Geometry. "km" or "miles". |
| lat | number | Geometry. New pin grid center latitude. |
| lng | number | Geometry. New pin grid center longitude. |
| regenerate_pins | boolean | Required when changing any geometry field on a scan that already has completed snapshots. Acknowledges that future snapshots will use a new pin grid and are not geometrically comparable with the historical trail. |
Example request
# Metadata-only update (always allowed)
curl -X PUT https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-... \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{ "location": "Acme Dental, Uptown" }'
# Geometry resize on a scan with snapshots
curl -X PUT https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-... \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"grid_size": 9,
"distance": 3.0,
"regenerate_pins": true
}'Example response 200 OK
{
"data": {
"uuid": "4f3a3b6c-...",
"location": "Acme Dental, Uptown",
"grid_size": 9,
"distance": 3.0
/* … */
}
}Notes
- On geometry changes the server regenerates selected_pins from the new lat/lng + grid_size + distance. Historical scan_results rows stay queryable by snapshot_at.
- A 422 geometry_change_requires_acknowledgement response means the scan has completed snapshots and regenerate_pins=true was not passed.
- Archived scans must be restored before update; PUT against an archived UUID returns 404.
/api/agency/v1/scans/{uuid}/archiveArchive or restore a scan
Archive (soft-delete) a scan to remove it from the active list and stop scheduled snapshots without losing history. Restore brings it back. Both endpoints are idempotent so retries are safe.
Example request
# Archive
curl -X POST https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-.../archive \
-H "Authorization: Bearer {token}"
# Restore
curl -X POST https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-.../restore \
-H "Authorization: Bearer {token}"
# List only archived scans (for restore workflows)
curl "https://app.your-white-label.com/api/agency/v1/scans?archived_only=true" \
-H "Authorization: Bearer {token}"
# Include archived alongside active in a normal list
curl "https://app.your-white-label.com/api/agency/v1/scans?include_archived=true" \
-H "Authorization: Bearer {token}"Example response 200 OK
{
"data": {
"uuid": "4f3a3b6c-...",
"is_scheduled": true,
"next_run_at": null
/* … */
}
}Notes
- Archive clears next_run_at so the cron skips the scan, but preserves frequency, scheduled_from, scheduled_to, scheduled_days, and timezone so a future restore re-arms cleanly.
- Restore on a scheduled scan recomputes next_run_at from the current clock.
- Idempotent: archiving an already-archived scan (or restoring an active one) returns 200 with the scan resource and no error.
/api/agency/v1/scans/{uuid}/scheduleSchedule recurring scans (set / get / unset)
POST sets or unsets a recurring schedule. GET on the same path returns just the schedule fields for audit workflows that iterate "list scans → check each schedule" without pulling the full scan resource. The cron worker picks up scheduled scans every 15 minutes and triggers a new snapshot whenever next_run_at falls inside the configured day-of-week and time-of-day window.
Request body
| Parameter | Type | Description |
|---|---|---|
| is_scheduled | boolean | true turns the schedule on, false disables it (required). |
| frequency | string | "weekly", "bi-weekly", or "monthly" (required when enabling). |
| scheduled_from | string | 24-hour HH:MM, e.g. "09:00" (required when enabling). |
| scheduled_to | string | 24-hour HH:MM, must be strictly later than scheduled_from. |
| scheduled_days | array<integer> | Day numbers (Carbon style: 0 = Sunday, 6 = Saturday). |
| timezone | string | PHP DateTimeZone identifier, e.g. "America/New_York". |
Example request
# Read the current schedule
curl https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-.../schedule \
-H "Authorization: Bearer {token}"
# Enable a Mon-Fri 9-5 monthly schedule in New York time
curl -X POST https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-.../schedule \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"is_scheduled": true,
"frequency": "monthly",
"scheduled_from": "09:00",
"scheduled_to": "17:00",
"scheduled_days": [1, 2, 3, 4, 5],
"timezone": "America/New_York"
}'
# Disable
curl -X POST https://app.your-white-label.com/api/agency/v1/scans/4f3a3b6c-.../schedule \
-H "Authorization: Bearer {token}" \
-d '{ "is_scheduled": false }'Example response 200 OK
{
"data": {
"uuid": "4f3a3b6c-...",
"is_scheduled": true,
"frequency": "monthly",
"next_run_at": "2026-06-01T13:00:00+00:00",
"scheduled_from": "09:00",
"scheduled_to": "17:00",
"scheduled_days": [1, 2, 3, 4, 5],
"timezone": "America/New_York"
}
}Notes
- The first scheduled run lands within ~15 minutes of next_run_at (the cron tick cadence).
- Disabling clears next_run_at but preserves your schedule fields, so re-enabling does not require re-entering them.
- A 422 invalid_schedule_window response means scheduled_from was not earlier than scheduled_to. A 422 invalid_timezone means the string is not a recognised DateTimeZone identifier.
Webhooks
Receive real-time HTTP notifications when events occur. Configure your webhook URL in Settings → API & Webhooks.
| Property | Value |
|---|---|
| HTTP Method | POST |
| Content-Type | application/json |
| User-Agent | ReviewManagement/V1 |
| Retry Policy | Up to 10 retries over 5 minutes with exponential backoff |
| Timeout | 30 seconds per attempt |
Events
| Event | Triggered when |
|---|---|
| review-created | A new review is received from any connected source |
| review-updated | An existing review is modified (reply added, rating changed) |
| private-feedback-created | A customer submits a private feedback form (low-rating, complaint, anything routed away from public reviews) |
| private-feedback-updated | An existing private feedback record is changed (reply added, rating or message edited) |
| organization-created | A new organisation is created |
| organization-updated | An organisation's details are modified |
| organization-deleted | An organisation is deleted |
| location-created | A new location is added |
| location-updated | A location's details are modified |
| location-deleted | A location is removed |
| scan-completed | A Local Search Grid scan finishes a full snapshot (agency-only) |
| scan-failed | A Local Search Grid scan run errored out before completing (agency-only) |
Example payload (review-created)
{
"webhook_event": "review-created",
"id": 142,
"organization_id": 1,
"location_id": 3,
"source_name": "google",
"identifier": "AbcXyz123",
"avatar": "https://lh3.googleusercontent.com/a/photo.jpg",
"author": "Sarah Johnson",
"rating": 5,
"original_rating": 5,
"title": null,
"message": "Outstanding service! The team went above and beyond.",
"url": "https://maps.google.com/review/...",
"verified": true,
"replied": false,
"reply_message": null,
"reply_url": null,
"hidden": false,
"reply_published_on": null,
"published_on": "2025-12-15T10:30:00+00:00",
"review_updated_on": null,
"updated_at": "2025-12-15T10:35:00+00:00",
"created_at": "2025-12-15T10:35:00+00:00",
"meta": {}
}Example payload (private-feedback-created)
Fired when a customer submits a private feedback form, and again on every update (typically when a team member replies). The meta object mirrors the raw form submission, so keys vary per form. Read the top-level fields for stable values.
{
"webhook_event": "private-feedback-created",
"id": 87,
"organization_id": 1,
"location_id": 3,
"rating": 2,
"author": "Alex Morgan",
"email": "alex@example.com",
"phone": "+15551234567",
"message": "The waiting time was longer than expected and I'd appreciate a call back.",
"replied": false,
"reply_message": null,
"reply_published_on": null,
"source": "email",
"source_name": "alex@example.com",
"updated_at": "2025-12-15T10:30:00+00:00",
"created_at": "2025-12-15T10:30:00+00:00",
"meta": {
"name": "Alex Morgan",
"email": "alex@example.com",
"phone": "+15551234567",
"message": "The waiting time was longer than expected and I'd appreciate a call back.",
"rating": 2,
"source": "email",
"source_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"source_name": "alex@example.com"
}
}Example payload (scan-completed)
Fired once a Local Search Grid snapshot finishes. Available to tenant-owner webhook configurations only.
{
"webhook_event": "scan-completed",
"uuid": "4f3a3b6c-...",
"status": "completed",
"snapshot_at": "2026-05-12T10:30:00+00:00",
"location": "Acme Dental, Downtown",
"keyword": "dentist near me",
"keywords": ["dentist near me", "emergency dentist"],
"grid_size": 7,
"summary": {
"average_rank": 5.2,
"top_3_count": 12,
"top_4_to_10_count": 23,
"not_ranking_count": 14
},
"report_url": "/scans/4f3a3b6c-.../report"
}Example payload (scan-failed)
Fired when a snapshot errors out before completing. The error_message is short and safe to display to your team.
{
"webhook_event": "scan-failed",
"uuid": "4f3a3b6c-...",
"status": "failed",
"error_message": "DataForSEO API key is missing",
"failed_at": "2026-05-12T10:31:00+00:00"
}AI Hub (MCP Server)
The AI Hub exposes the full read and write surface over the Model Context Protocol so AI clients like Claude Desktop, Cursor, and ChatGPT can query live review data, Google Business Profile metrics, and Local Search Grid scans. This is a separate endpoint from the REST API above, uses a different authentication ability (mcp:access), and speaks JSON-RPC 2.0 instead of REST.
Use the REST API for structured automations and pipelines. Use the MCP endpoint for interactive AI work. Many customers run both.
Endpoint
https://app.your-white-label.com/api/mcpProtocol
JSON-RPC 2.0Spec version
2025-06-18Rate limit
120/min token, 600/min workspaceSetup
Create an AI token in the dashboard under Settings → AI Hub, copy the generated config snippet, and paste it into your MCP client. For Claude Desktop, that's Settings → Developer → Edit Config.
{
"mcpServers": {
"my-agency-reviews": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://app.your-white-label.com/api/mcp",
"--header",
"Authorization: Bearer your-mcp-token"
]
}
}
}mcp-remote is a small stdio-to-HTTP bridge that lets desktop MCP clients talk to an HTTP server. It's the most reliable setup today because Claude Desktop's native URL config still has rough edges.
Authentication
Every request must include the bearer token in the Authorization header. The token must carry the mcp:access ability. Write tools also require mcp:writes, and PII-sensitive calls also require reviews:pii.
Authorization header
Authorization: Bearer your-mcp-tokenCorrelation IDs
Pass an X-Correlation-Id header (up to 64 chars) and the server will echo it back on the response and record it in the audit log. Useful for grouping every tool call from one AI prompt. Optional.
/api/mcp method: initializeinitialize
Handshake with the server, negotiate the protocol version, and receive the server capabilities and branded server name.
Example request
curl -X POST https://app.your-white-label.com/api/mcp \
-H "Authorization: Bearer your-mcp-token" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "Claude Desktop", "version": "1.0" }
}
}'Example response 200 OK
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": {} },
"serverInfo": {
"name": "Your Agency MCP Server",
"version": "1.0.0"
}
}
}/api/mcp method: tools/listtools/list
List the tools this token can invoke. The server filters the catalog by your token's abilities and permissions, so AI clients never see tools they cannot actually call.
The list you receive is filtered by the token's abilities. Customer tokens never see the agency-only Local Search Grid tools, even with write access enabled. Tenant-owner tokens see the full catalog, including the agency Local Search Grid suite.
Example request
curl -X POST https://app.your-white-label.com/api/mcp \
-H "Authorization: Bearer your-mcp-token" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }'Example response 200 OK
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "list_reviews",
"description": "List and filter reviews across organizations and locations...",
"inputSchema": { "type": "object", "properties": { ... } }
},
{
"name": "get_metrics",
"description": "Aggregate review metrics over a date range...",
"inputSchema": { "type": "object", "properties": { ... } }
}
]
}
}/api/mcp method: tools/calltools/call
Invoke one of the registered tools. The response contains both a text content block (for AI client display) and a structuredContent object with the parsed result.
Example request
curl -X POST https://app.your-white-label.com/api/mcp \
-H "Authorization: Bearer your-mcp-token" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Correlation-Id: client-prompt-20260421-abc123" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_metrics",
"arguments": {
"date_from": "2026-03-22",
"date_to": "2026-04-21",
"group_by": "source"
}
}
}'Example response 200 OK
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"structuredContent": {
"period": { "from": "2026-03-22", "to": "2026-04-21" },
"filters": { "organization_id": null, "location_id": null, "source_name": null },
"totals": {
"total_reviews": 487,
"avg_rating": 4.58,
"responded": 412,
"response_rate": 0.846,
"star_distribution": { "5": 362, "4": 79, "3": 21, "2": 12, "1": 13 },
"sentiment_distribution": { "positive": 441, "neutral": 21, "negative": 25 }
},
"breakdown": [
{ "bucket": "Google", "total_reviews": 318, "avg_rating": 4.61, "responded": 291, "response_rate": 0.915 },
{ "bucket": "Facebook", "total_reviews": 104, "avg_rating": 4.52, "responded": 78, "response_rate": 0.75 },
{ "bucket": "TripAdvisor", "total_reviews": 65, "avg_rating": 4.49, "responded": 43, "response_rate": 0.661 }
]
},
"content": [
{ "type": "text", "text": "{ \"period\": ..., \"totals\": ..., \"breakdown\": [...] }" }
]
}
}Tool catalog
Every tool exposed by the AI Hub, grouped by risk level. Pass the tool name to tools/call with the matching arguments. Required parameters are marked with an asterisk.
Read tools
(7 tools)list_reviews Requires reviews.viewFilter and list reviews across organisations and locations the caller can access. Supports filtering by source, date, rating, sentiment, response status, verified flag, and full-text search. Results are paginated.
| Param | Type | Description |
|---|---|---|
| organization_id | integer | Limit to one organisation |
| location_id | integer | Limit to one location (requires organization_id) |
| source_names | array<string> | Review platforms, e.g. ["Google", "Facebook"] |
| date_from | date | Inclusive start date (YYYY-MM-DD) |
| date_to | date | Inclusive end date (YYYY-MM-DD) |
| ratings | array<integer> | Star ratings to include, 1-5 |
| sentiment | string | "positive", "negative", or "neutral" |
| responded | boolean | Only reviews with or without a reply |
| verified | boolean | Only verified or unverified reviews |
| query | string | Full-text search over author, message, and reply |
| min_message_length | integer | Exclude reviews shorter than N characters |
| sort_by | string | Newest First | Oldest First | Highest Rating First | Lowest Rating First |
| page | integer | Page number (default 1) |
| per_page | integer | Rows per page (1-50, default 25) |
get_review Requires reviews.viewFetch a single review by ID with full detail including reply, tags, auto-respond history, and language.
| Param | Type | Description |
|---|---|---|
| review_id* | integer | The review ID to fetch |
list_organizations Requires locations.viewList organisations the caller can access, with optional review-count and avg-rating enrichment. Pass organization_id for full detail including locations.
| Param | Type | Description |
|---|---|---|
| organization_id | integer | Return full detail for one org (pagination ignored) |
| include_stats | boolean | Attach review count and avg rating. Default true |
| search | string | Substring match on org name |
| page | integer | Page number (default 1) |
| per_page | integer | Rows per page (1-50, default 25) |
list_locations Requires locations.viewList locations accessible to the caller with per-location review count and average rating. Scope to a single organisation with organization_id.
| Param | Type | Description |
|---|---|---|
| organization_id | integer | Limit to one organisation |
| page | integer | Page number (default 1) |
| per_page | integer | Rows per page (1-50, default 25) |
get_metrics Requires reviews.viewAggregate review metrics over a date range: totals, avg rating, star distribution, response rate, sentiment breakdown. Optional group_by slices the data. Pass compare_to_date_from and compare_to_date_to for period-over-period deltas.
| Param | Type | Description |
|---|---|---|
| organization_id | integer | Limit to one organisation |
| location_id | integer | Limit to one location |
| source_name | string | Filter by one source, e.g. "Google" |
| date_from | date | Inclusive start date (YYYY-MM-DD) |
| date_to | date | Inclusive end date (YYYY-MM-DD) |
| group_by | string | day | week | month | location | organization | source | rating | sentiment |
| compare_to_date_from | date | Baseline period start |
| compare_to_date_to | date | Baseline period end |
list_campaigns Requires campaigns.viewList review-request campaigns with paused state, schedule, location, contact list, and full funnel (invited, opened, clicked, reviewed, redirected, testimonials submitted, private feedback, unsubscribed, bounced) plus open, click, redirect, conversion, unsubscribe, and bounce rates.
| Param | Type | Description |
|---|---|---|
| campaign_id | integer | Return detailed metrics for one campaign |
| organization_id | integer | Limit to one organisation |
| date_from | date | Campaign created on or after |
| date_to | date | Campaign created on or before |
| page | integer | Page number |
| per_page | integer | Rows per page (1-50) |
get_ai_insights Requires reviews.viewRetrieve pre-computed AI analysis (themes, sentiment shifts, recommendations) for an organisation or location. Returns the most recent completed insight, or a specific period when period_start and period_end are provided.
| Param | Type | Description |
|---|---|---|
| organization_id* | integer | Target organisation |
| location_id | integer | Specific location (omit for org-wide insights only) |
| period_start | date | Start of the insight period |
| period_end | date | End of the insight period |
Read tools with PII access
(4 tools)list_private_feedback Requires reviews.viewList private feedback submissions (ratings and messages never published publicly). Name, email, and phone are redacted by default; pass include_pii=true to unlock them (requires reviews.pii on the token).
| Param | Type | Description |
|---|---|---|
| organization_id | integer | Limit to one organisation |
| location_id | integer | Limit to one location |
| date_from | date | Submitted on or after |
| date_to | date | Submitted on or before |
| ratings | array<integer> | Star ratings to include, 1-5 |
| include_pii | boolean | Unlock name/email/phone (requires reviews.pii) |
| responded | boolean | Only responded or unresponded |
| page | integer | Page number |
| per_page | integer | Rows per page (1-50) |
list_contacts Requires contacts.viewList review-request contacts with subscription state, latest status, and per-contact engagement counters (invites, opens, clicks, video plays, redirects, testimonials, private feedback, bounces, unsubscribes). Email, phone, and name redacted by default; pass include_pii=true to unlock (requires reviews.pii on the token).
| Param | Type | Description |
|---|---|---|
| organization_id | integer | Limit to one organisation |
| contact_list_id | integer | Limit to one contact list |
| subscribed | boolean | Only subscribed (true) or only unsubscribed (false) |
| date_from | date | Created on or after |
| date_to | date | Created on or before |
| include_pii | boolean | Unlock email/phone/name (requires reviews.pii) |
| page | integer | Page number |
| per_page | integer | Rows per page (1-50) |
get_contact_activity Requires contacts.viewFull timeline for one contact: every invite, open, click, video play, redirect, testimonial, private feedback, unsubscribe, bounce, and spam complaint, with the campaign, channel, step number, rating, message, and source attached. Contact name/email/phone are redacted by default; pass include_pii=true to unlock (requires reviews.pii on the token).
| Param | Type | Description |
|---|---|---|
| contact_id* | integer | The contact ID to load activity for |
| limit | integer | Max events to return, newest first (1-100, default 25) |
| include_pii | boolean | Unlock the contact's name, email, and phone (requires reviews.pii) |
list_auto_respond_rules Requires reviews.auto_respondList configured auto-respond rules with rating range, sources, delay, approval requirement, and AI usage. Use this to audit auto-respond coverage across accessible organisations.
| Param | Type | Description |
|---|---|---|
| organization_id | integer | Limit to one organisation |
| location_id | integer | Limit to one location |
| use_ai | boolean | Only AI-powered rules (true) or only static-template rules (false) |
| paused | boolean | Only paused (true) or only active (false) rules |
| page | integer | Page number |
| per_page | integer | Rows per page (1-50) |
Google Business Profile read tools
(2 tools)list_gbp_metrics Requires reviews.viewPull aggregated Google Business Profile performance metrics for one location and a date range: impressions (search, maps, desktop, mobile), call clicks, website clicks, and direction requests. Includes the equal-length previous period so AI clients can compute trend deltas in one call. Customer-accessible.
| Param | Type | Description |
|---|---|---|
| location_id* | integer | Internal location ID |
| from | date | Inclusive start date (YYYY-MM-DD). Defaults to 30 days ago. |
| to | date | Inclusive end date (YYYY-MM-DD). Defaults to yesterday. |
| include_daily | boolean | Include the per-day rows. Default false. |
list_gbp_search_terms Requires reviews.viewPull the search query terms shoppers used to find a location, with their monthly impression counts. Below-threshold rows (Google masks low-volume terms) are returned with below_threshold=true so AI clients can surface them without leaving gaps. Customer-accessible.
| Param | Type | Description |
|---|---|---|
| location_id* | integer | Internal location ID |
| year | integer | Defaults to current year. |
| month | integer | 1-12. Omit to return every month in the year. |
| per_page | integer | Rows per page (1-200, default 50) |
| page | integer | Page number (default 1) |
Local Search Grid tools (agency only)
(8 tools)list_scans Requires tenant ownerPaginated metadata for all Local Search Grid scans the agency owns, newest first. Filter by schedule, completion state, recency, or free-text search. Hidden from customer tokens.
| Param | Type | Description |
|---|---|---|
| search | string | Free-text over location and keyword |
| is_scheduled | boolean | Only recurring scans |
| completed | boolean | Only scans with a finished snapshot |
| failed | boolean | Only scans whose last run failed |
| since_days | integer | Scans created in the last N days (1-365) |
| page | integer | Page number |
| per_page | integer | 1-100, default 20 |
get_scan Requires tenant ownerReturn one scan with its per-pin results for the requested snapshot. Defaults to the most recent snapshot. top_n (0-20, default 5) caps the SERP list per pin so the response stays bounded on large grids. Use get_scan_data_points for paginated per-pin access.
| Param | Type | Description |
|---|---|---|
| uuid* | string | Scan UUID |
| snapshot_at | datetime | ISO-8601 timestamp; defaults to the latest snapshot |
| top_n | integer | SERP entries per pin (0-20). Default 5. Pass 20 for the full SERP list per pin. |
get_scan_data_points Requires tenant ownerPaginated per-pin data for one snapshot. Returns lat, lng, target_rank, and the top-N competitors (place_id, name, rank) at each pin. The right tool for rank-tracker integrations on large grids; pairs with the data_points REST endpoint.
| Param | Type | Description |
|---|---|---|
| uuid* | string | Scan UUID |
| snapshot_at | datetime | ISO-8601 timestamp; defaults to the latest snapshot for the (scan, keyword) combo |
| keyword | string | Restrict to one keyword on multi-keyword scans |
| top_n | integer | Competitors per pin (0-20). Default 5. Pass 0 for just lat/lng/target_rank. |
| page | integer | 1-based page |
| per_page | integer | 1-50, default 50 |
list_scan_snapshots Requires tenant ownerAggregated rank distribution and visibility score for every snapshot the scan has produced. One row per snapshot. Use this to plot a multi-month trend without paging through every pin.
| Param | Type | Description |
|---|---|---|
| uuid* | string | Scan UUID |
| keyword | string | Restrict the aggregate to one keyword on a multi-keyword scan |
get_scan_insights Requires tenant ownerEverything the in-app report computes for one snapshot: summary stats, long-range trends, snapshot-over-snapshot deltas, geographic quadrant strength, top competitors, and rule-based recommendations.
| Param | Type | Description |
|---|---|---|
| uuid* | string | Scan UUID |
| keyword | string | Restrict to one keyword on multi-keyword scans |
| snapshot_at | datetime | ISO-8601 timestamp; defaults to the latest snapshot |
create_scan Requires tenant ownerAlso requires mcp:writesCreate a brand-new scan, generate the hex-ring pin grid server-side, and queue the first snapshot. Returns immediately; subscribe to scan-completed for the result.
| Param | Type | Description |
|---|---|---|
| location* | string | Human-readable name for the dashboard |
| keywords* | array<string> | 1-20 keyword strings |
| grid_size* | integer | One of 3, 5, 7, 9, 11, 13, 15 |
| distance* | number | Distance from center to outer ring (> 0, ≤ 50) |
| distance_unit* | string | "km" or "miles" |
| address | string | Geocoded via Google Maps if lat/lng not supplied |
| lat | number | Pin grid center latitude |
| lng | number | Pin grid center longitude |
| target_place_id | string | Google Place ID for self-vs-competitor scoring |
| type | string | "business" (default) or "sab" |
run_scan Requires tenant ownerAlso requires mcp:writesSnap a new snapshot using the scan's saved grid, keywords, and distance. Returns immediately and fires scan-completed (or scan-failed) when finished.
| Param | Type | Description |
|---|---|---|
| uuid* | string | Scan UUID |
schedule_scan Requires tenant ownerAlso requires mcp:writesEnable or disable a recurring schedule on an existing scan. The cron worker picks up scheduled scans every 15 minutes and triggers a new snapshot whenever the next_run_at falls in the configured day-of-week and time-of-day window. Catches ranking shifts that only happen during business hours.
| Param | Type | Description |
|---|---|---|
| uuid* | string | Scan UUID |
| is_scheduled* | boolean | true turns the schedule on, false disables it |
| frequency | string | "weekly", "bi-weekly", or "monthly" |
| scheduled_from | string | 24-hour HH:MM start |
| scheduled_to | string | 24-hour HH:MM end (must be later than scheduled_from) |
| scheduled_days | array<integer> | Day numbers (0 = Sunday, 6 = Saturday) |
| timezone | string | PHP DateTimeZone identifier, e.g. "America/New_York" |
Write tools (always held for approval)
(2 tools)draft_review_response Requires reviews.respondAlso requires mcp:writesDraft a response to a review. The draft is always held for agency approval and never auto-sent. It appears in the Auto-Respond approvals queue for a human to review. Requires write access (mcp:writes) on the token.
| Param | Type | Description |
|---|---|---|
| review_id* | integer | The review to respond to |
| message* | string | The drafted reply (max 8000 chars). Held for approval; never auto-sent. |
tag_reviews Requires reviews.editAlso requires mcp:writesAttach or detach tags on a set of reviews. Useful for clustering reviews by theme after summarisation. Caps at 50 reviews per call. Requires write access (mcp:writes).
| Param | Type | Description |
|---|---|---|
| review_ids* | array<integer> | Up to 50 review IDs |
| attach_tags | array<string> | Tags to attach (max 64 chars each; reserved flag sentinel rejected) |
| detach_tags | array<string> | Tags to detach |
MCP error codes
MCP errors follow JSON-RPC 2.0. The server wraps all infrastructure errors (auth, rate limit, malformed JSON) in a JSON-RPC envelope instead of returning HTML, so AI clients can parse them cleanly.
| Code | Meaning | When |
|---|---|---|
| -32700 | Parse error | Request body is not valid JSON |
| -32600 | Invalid request | Missing method, wrong jsonrpc version, or malformed envelope |
| -32601 | Method not found | Unknown method, or tools/call pointing at a tool that does not exist |
| -32602 | Invalid params | Validation failed, unknown parameter, or missing required parameter |
| -32603 | Internal error | Unexpected server-side failure |
| -32001 | Unauthenticated | Token missing, invalid, or lacks mcp:access |
| -32001 | Missing permission | Token lacks the required ability (e.g. mcp:writes) or semantic permission |
| -32001 | Access denied | Passed an organization_id or location_id outside the caller's scope |
| -32002 | Not found | Tool resolved the request but no matching record exists |
| -32002 | Rate limit exceeded | Per-token or per-workspace rate limit hit; Retry-After header included |
Example error response
{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32001,
"message": "Your token is missing the required permission: reviews.respond."
}
}Guide available
For the full walkthrough on token creation, permissions, PII controls, write approvals, and the audit log, see AI Hub (MCP Server): Setup & Usage.