Skip to main content

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.

Base URLhttps://app.your-white-label.com/api/v1
AuthBearer Token
Rate Limit60 req/min
FormatJSON

Authentication

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-token

Token permissions

PermissionAccess
reviews:readView reviews and review sources
reviews:createCreate reviews
reviews:updateUpdate reviews
reviews:deleteDelete reviews
request:readView campaigns
request:createSend review invites
organizations:readView organisations
organizations:createCreate organisations
organizations:updateUpdate organisations
organizations:deleteDelete organisations

Rate limiting

60 requests per minute per token. Every response includes rate limit headers.

HeaderDescription
X-RateLimit-LimitMaximum requests per window (60)
X-RateLimit-RemainingRemaining requests in current window
Retry-AfterSeconds to wait before retrying (only on 429)

Errors

Standard HTTP status codes. Errors return JSON with a message field.

CodeStatusDescription
200OKRequest succeeded
201CreatedResource successfully created
202AcceptedRequest accepted and queued
401UnauthenticatedMissing or invalid API token
403ForbiddenToken lacks required permissions
404Not FoundResource does not exist
422Validation ErrorRequest body failed validation
429Too Many RequestsRate limit exceeded
500Server ErrorSomething 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."
    ]
  }
}
GET/api/v1/reviews

List reviews

Retrieve a paginated list of reviews across all connected sources. Filter by rating, organisation, location, or review source.

Requires reviews:read

Query parameters

ParameterTypeDescription
ratingintegerFilter by rating (1-5)
organization_idintegerFilter by organisation
location_idintegerFilter by location
source_names[]arrayFilter by source names (e.g. Google, Facebook)
pageintegerPage 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 }
}
GET/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
  }
}
POST/api/v1/reviews

Create 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

ParameterTypeDescription
authorstringReviewer name (required, max 255 chars)
ratingintegerStar rating 1-5 (required)
published_ondatePublication date in Y-m-d format (required)
organization_idintegerOrganisation ID (required)
location_idintegerLocation ID, must belong to the organisation (required)
sourceintegerIntegration ID from the Writable Sources endpoint (required)
messagestringReview content
titlestringReview title (max 255 chars)
avatarfileReviewer 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.

PUT/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

ParameterTypeDescription
authorstringReviewer name (max 255 chars)
ratingintegerStar rating 1-5
messagestringReview content
titlestringReview title (max 255 chars)
published_ondatePublication date in Y-m-d format
avatarfileReviewer 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.

DELETE/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.

GET/api/v1/reviews/sources

Writable 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

ParameterTypeDescription
organization_idintegerFilter by organisation
location_idintegerFilter by location
pageintegerPage 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": { ... }
}
GET/api/v1/organizations

List 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": { ... }
}
POST/api/v1/organizations

Create organisation

Create a new organisation. The name must be unique across your account.

Requires organizations:create

Request body

ParameterTypeDescription
namestringOrganisation name (required, max 255 chars, unique)
logofileLogo 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"
}
GET/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"
}
PUT/api/v1/organizations/{id}

Update organisation

Update an existing organisation's name or logo.

Requires organizations:update

Request body

ParameterTypeDescription
namestringOrganisation name (max 255 chars, unique)
logofileLogo 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"}'
DELETE/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."
}
GET/api/v1/request-reviews/campaigns

List campaigns

Retrieve your review request campaigns. Use campaign UUIDs to send invites via the Send Invite endpoint.

Requires request:read

Query parameters

ParameterTypeDescription
organization_idintegerFilter campaigns by organisation
searchstringSearch 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": { ... }
}
POST/api/v1/request-reviews/campaigns/{campaign}/invite

Send 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

ParameterTypeDescription
campaignuuidCampaign UUID (required path parameter, from List Campaigns)
emailstringContact email. Required when phone is not provided.
phonestringPhone in E.164 format (e.g. +14275238194). Required when email is not provided.
first_namestringContact first name (max 255 chars)
last_namestringContact last name (max 255 chars)
send_after_hoursintegerHours 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.

GET/api/v1/sources

List 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
    }
  ]
}
GET/api/v1/countries

List 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" },
  ...
]
GET/api/v1/me

Current 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"
}
GET/api/v1/gbp/metrics

Google 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

ParameterTypeDescription
location_idintegerInternal location ID (required, from /api/v1/locations).
fromdateInclusive start date in Y-m-d format. Defaults to 30 days ago.
todateInclusive end date in Y-m-d format. Defaults to yesterday.
include_dailybooleanInclude 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.

GET/api/v1/gbp/search-terms

GBP 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

ParameterTypeDescription
location_idintegerInternal location ID (required).
yearintegerDefaults to the current year.
monthinteger1-12. Omit to return every month in the year.
per_pageintegerDefault 50, max 200.
pageinteger1-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/v1

Agency tokens have full access across all customer accounts. There are no scoped permissions here, so the token acts as a super admin.

POST/api/agency/v1/customers

Create 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

ParameterTypeDescription
namestringCustomer name (required)
companystringOrganisation/company name (required)
emailstringLogin email address (required)
passwordstringPassword, min 8 characters (required)
custom_plan_idstringPlan ID to assign (required)
phonestringPhone in E.164 format (optional)
location_limitintegerOverride plan location limit (optional, min 1)
member_limitintegerOverride plan member limit. 0 = unlimited (optional)
send_invitebooleanEmail login details to the customer (optional)
send_google_connect_linkbooleanEmail 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_linkbooleanSame as send_google_connect_link but for Facebook Pages. Independent of the Google flag: send one, the other, both, or neither (optional).
countrystringISO 3166-1 alpha-2 code. Required if Stripe Tax is enabled.
postal_codestringRequired 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.
PUT/api/agency/v1/customers/{customer}

Switch customer plan

Change the customer's assigned plan.

Request body

ParameterTypeDescription
custom_plan_idstringThe 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" }]
  }
}
PUT/api/agency/v1/customers/{customer}/location-limit

Update location & member limits

Override the customer's location or member limit independently of their plan defaults. Two separate endpoints.

Request body

ParameterTypeDescription
location_limitintegerPUT .../location-limit: number of locations allowed (min 1)
member_limitintegerPUT .../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." }
POST/api/agency/v1/customers/{customer}/add-credits

Add credits

Add email or SMS credits to a customer's account. Credits can optionally expire.

Request body

ParameterTypeDescription
typestringCredit type: "email" or "sms" (required)
creditsintegerNumber of credits to add, min 1 (required)
expires_atdateExpiry date in Y-m-d format (optional)
notesstringNote 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
}
PUT/api/agency/v1/customers/{customer}/pause

Pause & 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." }
DELETE/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." }
GET/api/agency/v1/customers

List all customers

Retrieve a paginated list of all your customers with their plans and organisations.

Parameters

ParameterTypeDescription
searchstringSearch 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 }
}
GET/api/agency/v1/custom-plans

List 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"
    }
  ]
}
GET/api/agency/v1/orders

Orders (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/..."]
        }
      ]
    }
  ]
}
GET/api/agency/v1/scans

List 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

ParameterTypeDescription
searchstringFree-text over location and keyword.
is_scheduledbooleanOnly recurring scans.
completedbooleanOnly scans with a finished snapshot.
failedbooleanOnly scans whose last run failed.
since_daysintegerLimit to scans created in the last N days (1-365).
per_pageinteger1-100, default 20.
pageinteger1-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.
GET/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

ParameterTypeDescription
uuidstringScan UUID (required path parameter).
snapshot_atdatetimeISO-8601 timestamp from available_snapshots. Defaults to the latest snapshot.
top_nintegerSERP 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.
GET/api/agency/v1/scans/{uuid}/data_points

LSG 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

ParameterTypeDescription
uuidstringScan UUID (required path parameter).
snapshot_atdatetimeISO-8601 timestamp. Defaults to the latest snapshot for the (scan, keyword) combo.
keywordstringRestrict to one keyword on multi-keyword scans.
top_nintegerCompetitors per pin (0-20). Default 5. Pass 0 for just lat/lng/target_rank.
per_pageinteger1-500. Default 100.
pageinteger1-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.
GET/api/agency/v1/scans/{uuid}/snapshots

Snapshot 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

ParameterTypeDescription
uuidstringScan UUID (required path parameter).
keywordstringRestrict 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).
GET/api/agency/v1/scans/{uuid}/insights

Deep 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

ParameterTypeDescription
uuidstringScan UUID (required path parameter).
keywordstringRestrict to one keyword on multi-keyword scans.
snapshot_atdatetimeISO-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.
POST/api/agency/v1/scans

Create 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

ParameterTypeDescription
locationstringHuman-readable name (required). Shows in the dashboard.
addressstringUsed only if lat/lng are not supplied (server geocodes via Google Maps).
keywordsarray<string>1-20 keyword strings (required).
grid_sizeintegerOne of 3, 5, 7, 9, 11, 13, 15 (required).
distancenumberDistance from the center to the outer ring, greater than 0, max 50 (required).
distance_unitstring"km" or "miles" (required).
typestring"business" (default) or "sab" for service-area businesses.
target_place_idstringGoogle Place ID, used to score "self" pins in competitor analysis.
latnumberPin grid center latitude.
lngnumberPin 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).
POST/api/agency/v1/scans/{uuid}/run

Re-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.
PUT/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

ParameterTypeDescription
locationstringDisplay name shown in the dashboard.
addressstringDisplay-only.
keywordsarray<string>Full replacement set, 1-20 strings, each ≤ 100 chars.
target_place_idstringGoogle Place ID. Used to score "self" in competitor analysis.
typestring"business" or "sab".
grid_sizeintegerGeometry. One of 3, 5, 7, 9, 11, 13, 15.
distancenumberGeometry. > 0 and ≤ 50.
distance_unitstringGeometry. "km" or "miles".
latnumberGeometry. New pin grid center latitude.
lngnumberGeometry. New pin grid center longitude.
regenerate_pinsbooleanRequired 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.
POST/api/agency/v1/scans/{uuid}/archive

Archive 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.
POST/api/agency/v1/scans/{uuid}/schedule

Schedule 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

ParameterTypeDescription
is_scheduledbooleantrue turns the schedule on, false disables it (required).
frequencystring"weekly", "bi-weekly", or "monthly" (required when enabling).
scheduled_fromstring24-hour HH:MM, e.g. "09:00" (required when enabling).
scheduled_tostring24-hour HH:MM, must be strictly later than scheduled_from.
scheduled_daysarray<integer>Day numbers (Carbon style: 0 = Sunday, 6 = Saturday).
timezonestringPHP 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.

PropertyValue
HTTP MethodPOST
Content-Typeapplication/json
User-AgentReviewManagement/V1
Retry PolicyUp to 10 retries over 5 minutes with exponential backoff
Timeout30 seconds per attempt

Events

EventTriggered when
review-createdA new review is received from any connected source
review-updatedAn existing review is modified (reply added, rating changed)
private-feedback-createdA customer submits a private feedback form (low-rating, complaint, anything routed away from public reviews)
private-feedback-updatedAn existing private feedback record is changed (reply added, rating or message edited)
organization-createdA new organisation is created
organization-updatedAn organisation's details are modified
organization-deletedAn organisation is deleted
location-createdA new location is added
location-updatedA location's details are modified
location-deletedA location is removed
scan-completedA Local Search Grid scan finishes a full snapshot (agency-only)
scan-failedA 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"
}
Model Context Protocol

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/mcp

Protocol

JSON-RPC 2.0

Spec version

2025-06-18

Rate limit

120/min token, 600/min workspace

Setup

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-token

Correlation 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.

POST/api/mcp method: initialize

initialize

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"
    }
  }
}
POST/api/mcp method: tools/list

tools/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": { ... } }
      }
    ]
  }
}
POST/api/mcp method: tools/call

tools/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.view

Filter 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.

ParamTypeDescription
organization_idintegerLimit to one organisation
location_idintegerLimit to one location (requires organization_id)
source_namesarray<string>Review platforms, e.g. ["Google", "Facebook"]
date_fromdateInclusive start date (YYYY-MM-DD)
date_todateInclusive end date (YYYY-MM-DD)
ratingsarray<integer>Star ratings to include, 1-5
sentimentstring"positive", "negative", or "neutral"
respondedbooleanOnly reviews with or without a reply
verifiedbooleanOnly verified or unverified reviews
querystringFull-text search over author, message, and reply
min_message_lengthintegerExclude reviews shorter than N characters
sort_bystringNewest First | Oldest First | Highest Rating First | Lowest Rating First
pageintegerPage number (default 1)
per_pageintegerRows per page (1-50, default 25)
get_review Requires reviews.view

Fetch a single review by ID with full detail including reply, tags, auto-respond history, and language.

ParamTypeDescription
review_id*integerThe review ID to fetch
list_organizations Requires locations.view

List organisations the caller can access, with optional review-count and avg-rating enrichment. Pass organization_id for full detail including locations.

ParamTypeDescription
organization_idintegerReturn full detail for one org (pagination ignored)
include_statsbooleanAttach review count and avg rating. Default true
searchstringSubstring match on org name
pageintegerPage number (default 1)
per_pageintegerRows per page (1-50, default 25)
list_locations Requires locations.view

List locations accessible to the caller with per-location review count and average rating. Scope to a single organisation with organization_id.

ParamTypeDescription
organization_idintegerLimit to one organisation
pageintegerPage number (default 1)
per_pageintegerRows per page (1-50, default 25)
get_metrics Requires reviews.view

Aggregate 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.

ParamTypeDescription
organization_idintegerLimit to one organisation
location_idintegerLimit to one location
source_namestringFilter by one source, e.g. "Google"
date_fromdateInclusive start date (YYYY-MM-DD)
date_todateInclusive end date (YYYY-MM-DD)
group_bystringday | week | month | location | organization | source | rating | sentiment
compare_to_date_fromdateBaseline period start
compare_to_date_todateBaseline period end
list_campaigns Requires campaigns.view

List 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.

ParamTypeDescription
campaign_idintegerReturn detailed metrics for one campaign
organization_idintegerLimit to one organisation
date_fromdateCampaign created on or after
date_todateCampaign created on or before
pageintegerPage number
per_pageintegerRows per page (1-50)
get_ai_insights Requires reviews.view

Retrieve 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.

ParamTypeDescription
organization_id*integerTarget organisation
location_idintegerSpecific location (omit for org-wide insights only)
period_startdateStart of the insight period
period_enddateEnd of the insight period

Read tools with PII access

(4 tools)
list_private_feedback Requires reviews.view

List 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).

ParamTypeDescription
organization_idintegerLimit to one organisation
location_idintegerLimit to one location
date_fromdateSubmitted on or after
date_todateSubmitted on or before
ratingsarray<integer>Star ratings to include, 1-5
include_piibooleanUnlock name/email/phone (requires reviews.pii)
respondedbooleanOnly responded or unresponded
pageintegerPage number
per_pageintegerRows per page (1-50)
list_contacts Requires contacts.view

List 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).

ParamTypeDescription
organization_idintegerLimit to one organisation
contact_list_idintegerLimit to one contact list
subscribedbooleanOnly subscribed (true) or only unsubscribed (false)
date_fromdateCreated on or after
date_todateCreated on or before
include_piibooleanUnlock email/phone/name (requires reviews.pii)
pageintegerPage number
per_pageintegerRows per page (1-50)
get_contact_activity Requires contacts.view

Full 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).

ParamTypeDescription
contact_id*integerThe contact ID to load activity for
limitintegerMax events to return, newest first (1-100, default 25)
include_piibooleanUnlock the contact's name, email, and phone (requires reviews.pii)
list_auto_respond_rules Requires reviews.auto_respond

List configured auto-respond rules with rating range, sources, delay, approval requirement, and AI usage. Use this to audit auto-respond coverage across accessible organisations.

ParamTypeDescription
organization_idintegerLimit to one organisation
location_idintegerLimit to one location
use_aibooleanOnly AI-powered rules (true) or only static-template rules (false)
pausedbooleanOnly paused (true) or only active (false) rules
pageintegerPage number
per_pageintegerRows per page (1-50)

Google Business Profile read tools

(2 tools)
list_gbp_metrics Requires reviews.view

Pull 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.

ParamTypeDescription
location_id*integerInternal location ID
fromdateInclusive start date (YYYY-MM-DD). Defaults to 30 days ago.
todateInclusive end date (YYYY-MM-DD). Defaults to yesterday.
include_dailybooleanInclude the per-day rows. Default false.
list_gbp_search_terms Requires reviews.view

Pull 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.

ParamTypeDescription
location_id*integerInternal location ID
yearintegerDefaults to current year.
monthinteger1-12. Omit to return every month in the year.
per_pageintegerRows per page (1-200, default 50)
pageintegerPage number (default 1)

Local Search Grid tools (agency only)

(8 tools)
list_scans Requires tenant owner

Paginated 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.

ParamTypeDescription
searchstringFree-text over location and keyword
is_scheduledbooleanOnly recurring scans
completedbooleanOnly scans with a finished snapshot
failedbooleanOnly scans whose last run failed
since_daysintegerScans created in the last N days (1-365)
pageintegerPage number
per_pageinteger1-100, default 20
get_scan Requires tenant owner

Return 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.

ParamTypeDescription
uuid*stringScan UUID
snapshot_atdatetimeISO-8601 timestamp; defaults to the latest snapshot
top_nintegerSERP entries per pin (0-20). Default 5. Pass 20 for the full SERP list per pin.
get_scan_data_points Requires tenant owner

Paginated 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.

ParamTypeDescription
uuid*stringScan UUID
snapshot_atdatetimeISO-8601 timestamp; defaults to the latest snapshot for the (scan, keyword) combo
keywordstringRestrict to one keyword on multi-keyword scans
top_nintegerCompetitors per pin (0-20). Default 5. Pass 0 for just lat/lng/target_rank.
pageinteger1-based page
per_pageinteger1-50, default 50
list_scan_snapshots Requires tenant owner

Aggregated 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.

ParamTypeDescription
uuid*stringScan UUID
keywordstringRestrict the aggregate to one keyword on a multi-keyword scan
get_scan_insights Requires tenant owner

Everything 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.

ParamTypeDescription
uuid*stringScan UUID
keywordstringRestrict to one keyword on multi-keyword scans
snapshot_atdatetimeISO-8601 timestamp; defaults to the latest snapshot
create_scan Requires tenant ownerAlso requires mcp:writes

Create 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.

ParamTypeDescription
location*stringHuman-readable name for the dashboard
keywords*array<string>1-20 keyword strings
grid_size*integerOne of 3, 5, 7, 9, 11, 13, 15
distance*numberDistance from center to outer ring (> 0, ≤ 50)
distance_unit*string"km" or "miles"
addressstringGeocoded via Google Maps if lat/lng not supplied
latnumberPin grid center latitude
lngnumberPin grid center longitude
target_place_idstringGoogle Place ID for self-vs-competitor scoring
typestring"business" (default) or "sab"
run_scan Requires tenant ownerAlso requires mcp:writes

Snap a new snapshot using the scan's saved grid, keywords, and distance. Returns immediately and fires scan-completed (or scan-failed) when finished.

ParamTypeDescription
uuid*stringScan UUID
schedule_scan Requires tenant ownerAlso requires mcp:writes

Enable 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.

ParamTypeDescription
uuid*stringScan UUID
is_scheduled*booleantrue turns the schedule on, false disables it
frequencystring"weekly", "bi-weekly", or "monthly"
scheduled_fromstring24-hour HH:MM start
scheduled_tostring24-hour HH:MM end (must be later than scheduled_from)
scheduled_daysarray<integer>Day numbers (0 = Sunday, 6 = Saturday)
timezonestringPHP DateTimeZone identifier, e.g. "America/New_York"

Write tools (always held for approval)

(2 tools)
draft_review_response Requires reviews.respondAlso requires mcp:writes

Draft 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.

ParamTypeDescription
review_id*integerThe review to respond to
message*stringThe drafted reply (max 8000 chars). Held for approval; never auto-sent.
tag_reviews Requires reviews.editAlso requires mcp:writes

Attach 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).

ParamTypeDescription
review_ids*array<integer>Up to 50 review IDs
attach_tagsarray<string>Tags to attach (max 64 chars each; reserved flag sentinel rejected)
detach_tagsarray<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.

CodeMeaningWhen
-32700Parse errorRequest body is not valid JSON
-32600Invalid requestMissing method, wrong jsonrpc version, or malformed envelope
-32601Method not foundUnknown method, or tools/call pointing at a tool that does not exist
-32602Invalid paramsValidation failed, unknown parameter, or missing required parameter
-32603Internal errorUnexpected server-side failure
-32001UnauthenticatedToken missing, invalid, or lacks mcp:access
-32001Missing permissionToken lacks the required ability (e.g. mcp:writes) or semantic permission
-32001Access deniedPassed an organization_id or location_id outside the caller's scope
-32002Not foundTool resolved the request but no matching record exists
-32002Rate limit exceededPer-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.