Primitive/business/places4 min read

Reviews & Listings gets a second provider: normalized places and reviews from Google Maps

Two new endpoints on the Reviews & Listings primitive return normalized place and review objects from Google Maps, name the provider that answered, and bill 3× what the run actually cost — with contact scraping and reviewer identity switched off at the source.

Reviews & listings primitive →Read the docs →

Primitive/business/places
TL;DR
  • POST /v1/business/places/search and /v1/business/places/reviews normalized places and reviews from Google Maps
  • Reviews & Listings was single-vendor; a second provider means an outage or coverage gap is no longer a dead end
  • These two return normalized objects and NAME the provider, unlike the DataForSEO passthrough routes beside them
  • No silent fallback: an unconfigured provider is a refusal that names the alternative, not another vendor's data in the same fields
  • Contact-person scraping is never requested and reviewer identity is switched off explicitly this is not a route to personal data

Reviews & Listings had exactly one vendor behind it. Every listing, every rating, every review came from DataForSEO. That is a fine way to ship a primitive and a poor way to run one: a vendor outage was a Naive outage, a coverage gap had no second opinion, and there was nothing to check a rating against.

Google Maps is now a second provider on the same primitive.

naive business places search --query "dentist" --location "Austin, Texas" --limit 25
naive business places reviews --place-ids ChIJ... --max-reviews 50 --sort newest

Normalized, because "second provider" has to mean something

The existing routes on this primitive are a passthrough: they hand back DataForSEO's own tasks[] envelope. That shape cannot carry two vendors. The envelope is one vendor's, the async task lifecycle is one vendor's, and the price is a list price rather than a cost.

So the two new endpoints return provider-agnostic place and review objects — place_id, name, categories, address, coordinates, rating, review count, open/closed — and name the provider that answered in every response. Without that, a second provider is an implementation detail no caller can see, reason about, or reproduce a result from.

One small honesty detail: rating is absent on a listing with no reviews rather than 0. A new business and a badly-reviewed one are different facts, and a default value would merge them.

No silent fallback

The obvious feature request here is automatic failover between the two providers. We did not build it.

A fallback has to be silent to be useful, and silent is exactly wrong on a primitive whose whole subject is where a review came from. It would also be a lie about shape: the DataForSEO equivalents are task-based, so "falling back" from a synchronous call would hand you a task id where you asked for data.

When the Maps provider has no credential, you get feature_not_configured naming the DataForSEO routes to use instead. A refusal you can act on beats another vendor's data wearing the same field names.

Two things this deliberately does not return

The provider will sell us more than we ask for. Both refusals are governance decisions, not scope decisions.

No contact people. The places crawler will also scrape named contact persons with work emails and mobile numbers, at 25× the price of a place row. That is People data — it carries an opt-in gate, a B2B-only field filter and an erasure path, none of which a listings lookup has. The inputs that request it are never sent, so a local-search endpoint cannot become a back door into personal data.

No reviewer identity. The reviews actor defaults to attaching reviewer names, profile URLs, photos and cross-business review histories. We send personalData: false explicitly rather than relying on that default holding still in someone else's release. You get the review — rating, text, date, language, the owner's reply. A reviewer's review history across businesses is a behavioural profile of a private individual, and nothing in this primitive's terms covers building one.

A default is a thing that changes when you are not looking. Sending the value explicitly is the difference between a policy and a hope.

Priced on what the run cost

Both endpoints bill 3× the provider spend the run actually incurred. A place row and a review row cost very different amounts at the provider — reviews are roughly a seventh of a place — and deriving the charge from real usage reflects that instead of averaging it into a block rate that is wrong in both directions.

  • An empty result is still billed. A search that matched nothing was still a provider run on our invoice, so it is charged at the same 3×.
  • A provider failure is never billed. The charge settles after output exists.
  • A projection from the provider's listed per-event price is reserved before the run, so an unaffordable job is refused rather than half-run.
  • provider_usd lands on the ledger entry behind every charge, next to the multiplier it was priced at, so a usage row can be checked rather than taken on faith.

Watch max_reviews_per_place: it multiplies. Twenty-five places at a hundred reviews each is 2,500 billed rows from one call.

Still not Company Data

Reviews & Listings is a company's local presence and reputation. Company Data is the company itself — headcount, funding, tech stack. They are siblings, and the label on this primitive changed from "Business Data" precisely so the two stopped reading as two names for one product.

Where to start

  • Places & Reviews docs
  • naive business places search --query "what" --location "where"
  • MCP: naive_business_places_search, naive_business_places_reviews
Frequently Asked Questions
Is this a new primitive?+
No. It is a second provider and two new endpoints on the existing business primitive, labelled Reviews & Listings.
How is it different from the existing Google endpoints?+
The existing routes proxy DataForSEO and return that vendor's task envelope. These two return normalized place and review objects, synchronously, and name the provider that answered.
Does it fall back to DataForSEO?+
No, deliberately. A silent fallback would hide provenance on a primitive whose subject is provenance, and would return a task id where a caller expected data. An unconfigured provider is an explicit refusal that names the alternative.
Does it return contact details for a business's staff?+
No. The provider offers it; we never send that input. Contact data belongs to the People primitive, which carries an opt-in gate, a B2B-only field filter and an erasure path.
Why is location required?+
Maps results are location-scoped. Without a location, results depend on where the provider run happened — a result you cannot reproduce and we cannot explain.
A
AshleyEngineering

Engineering at Naïve. Owns the data primitives.

@ashley_naive
Keep reading