Yelp via Scavio
Search Yelp businesses in a metro, read one business in full, and page through its reviews. All endpoints return structured JSON.
When to trigger
Use this skill when the user asks to:
- Find local businesses by term and location, in Yelp's ranked order
- Filter by price band, open-now, or Yelp's own attribute aliases
- Pull one business in full - rating histogram, price band, categories, address and coordinates, phone, website and menu links, hours and holidays, amenities, photos, popular items, health inspections, Q&A, licences, claim status
- Read review bodies with author expertise, attached photos, reaction counts and the owner's response
- Build local-lead lists, reputation monitors or competitor comp sets
Setup
Get a free API key at scavio.dev (50 free credits to get started, no card required):
export SCAVIO_API_KEY=sk_live_your_key
If you do not have a key yet
An agent running this skill without SCAVIO_API_KEY set will get 401 on every
call below. The whole path from nothing to a working key is self-serve:
- Sign up at dashboard.scavio.dev - email or Google, no card required.
- A key is created for the account automatically at signup. It is on the dashboard under API Keys, ready to copy.
- A new account starts with 50 credits. Endpoints in this skill cost 1 credit each unless the table below says otherwise.
When the balance runs out the API answers 402 with a JSON body carrying
billing_url. Topping up needs no code change - the same key keeps working.
The smallest purchase is 2,500 credits for $25, and monthly plans work out
cheaper per credit if the usage is steady rather than one-off.
Every request is a POST with a JSON body and:
Authorization: Bearer $SCAVIO_API_KEY
Endpoints
Base URL: https://api.scavio.dev. Every Yelp endpoint costs 2 credits.
| Endpoint | Credits | What it returns |
|---|---|---|
POST /api/v1/yelp/search |
2 | Businesses in Yelp's ranked order: rating, review count, price band, categories, address, contact rails, hours, photos, review snippet; each row carries business_id and alias |
POST /api/v1/yelp/business |
2 | One business in full - plus the first page of reviews at no extra cost |
POST /api/v1/yelp/reviews |
2 | A page of reviews: rating, full text, language, author profile and expertise counts, attached photos, reaction counts, owner response |
Workflow
- Find businesses: call
/yelp/searchwithtermandlocation(or a fullyelp.com/searchURL asurl). Readbusinesses[].business_idoralias. - One business: call
/yelp/businesswith thatbusiness_id- an alias likedesnudo-coffee-austin-2, the opaque encid, or ayelp.com/bizURL all work. This already includes page 1 of the reviews. - More reviews: call
/yelp/reviewsstarting atpage: 2.
The two ways to waste 2 credits
/yelp/reviewswithpage: 1re-fetches the document/businessalready returned. It is the same content for another 2 credits. Always start review paging at page 2.- A search without
locationhas no fixed place, so the same request reports on a different metro from one run to the next.locationis effectively required even though the schema allows aurlinstead.
Pagination
Yelp fixes the page size at 10 for both search and reviews.
/searchtakes a 1-basedpage.countis the 10-row page,total_resultsis Yelp's headline count,results_per_pageandstartare echoed back./reviewstakes a 1-basedpageand carrieshas_next_page. A page past the last review is a 404, not an empty result - stop onhas_next_page: false./businesstakes no paging parameter.
Parameters
Search (/search)
| Parameter | Type | Default | Description |
|---|---|---|---|
term |
string | with location |
What to look for (1-200 chars) |
location |
string | with term |
Metro, neighbourhood or address (1-200 chars) |
url |
string | alternative | Full yelp.com/search URL instead of term + location (1-1000 chars) |
page |
integer | -- | 1-based, page size fixed at 10 |
sort |
string | recommended |
recommended, rating, review_count |
price |
integer[] | -- | Any of 1, 2, 3, 4 (1-4 items) |
open_now |
boolean | -- | Open at request time |
attributes |
string[] | -- | Raw Yelp filter aliases, max 20 (RestaurantsDelivery, GoodForKids, WheelchairAccessible) |
(term AND location) or url is required.
sort is closed - Yelp ignores an unrecognised sort and serves default ranking under a 200, so an invented value buys a 2-credit call for a sort that never ran. attributes is the opposite: a deliberate passthrough, because Yelp's vocabulary runs to roughly 117 aliases per vertical. An alias Yelp does not know is ignored upstream and the results come back unfiltered - so check that the filter you asked for is reflected in the results before reporting it as applied.
Business (/business)
| Parameter | Type | Default | Description |
|---|---|---|---|
business_id |
string | one of | Alias (desnudo-coffee-austin-2), opaque encid, or a yelp.com/biz URL |
url |
string | one of | Full yelp.com/biz URL |
Reviews (/reviews)
| Parameter | Type | Default | Description |
|---|---|---|---|
business_id |
string | one of | Alias, encid, or biz URL |
url |
string | one of | Full yelp.com/biz URL |
page |
integer | -- | Start at 2 - page 1 is what /business already returned |
sort |
string | relevance |
relevance, newest, oldest, rating_high, rating_low, elites |
rating |
integer | -- | 1-5. Changes filtered_review_count, not review_count |
Examples
import requests
BASE = "https://api.scavio.dev"
# Your key from https://scavio.dev. Load it from your environment or secret
# store in real code - keep it out of source control.
API_KEY = "sk_your_key_here"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# 1. Always send location - a location-less search drifts between metros
found = requests.post(f"{BASE}/api/v1/yelp/search", headers=HEADERS,
json={"term": "coffee", "location": "Austin, TX",
"sort": "review_count", "price": [1, 2], "open_now": True}).json()
row = found["data"]["businesses"][0]
# 2. Full business record - page 1 of the reviews rides along free
biz = requests.post(f"{BASE}/api/v1/yelp/business", headers=HEADERS,
json={"business_id": row["alias"]}).json()["data"]
print(biz["rating"], biz["review_count"], biz["not_recommended_review_count"])
first_page_reviews = biz["reviews"]
# 3. Page PAST it. Never ask for page 1 here - it is the same 2 credits twice.
more = requests.post(f"{BASE}/api/v1/yelp/reviews", headers=HEADERS,
json={"business_id": row["alias"], "page": 2, "sort": "newest"}).json()["data"]
# 4. One-star reviews only: filtered_review_count is the count that matches
one_star = requests.post(f"{BASE}/api/v1/yelp/reviews", headers=HEADERS,
json={"business_id": row["alias"], "rating": 1, "page": 2}).json()["data"]
print(one_star["filtered_review_count"], "of", one_star["review_count"])
Paging reviews, stopping on the signal Yelp actually gives:
def review_pages(business_id, start=2, max_pages=4, **body):
"""10 per page, 2 credits per page. Page 1 came free with /business."""
out = []
for page in range(start, start + max_pages):
data = requests.post(f"{BASE}/api/v1/yelp/reviews", headers=HEADERS,
json={"business_id": business_id, "page": page, **body}).json()["data"]
out += data["reviews"]
if not data["has_next_page"]: # past the last page is a billed 404
break
return out
Response shapes
Every response uses the envelope { data, response_time, credits_used, credits_remaining }. Key data fields:
- search —
term,location,url,location_display,city,state,country,latitude,longitude,bounds,categories,vertical,sort,attributes,open_now,breadcrumbs,total_results,results_per_page,page,start,related_searches,count,businesses[](pos,business_id,alias,name,url,rank,is_ad,rating,review_count,price_range,categories,address,street_address,city,state,postal_code,country,neighborhoods,timezone,hours,image,images,photos,snippet,tags,actions,highlights,is_yelp_guaranteed,has_verified_license,is_service_area_business). - business —
business_id,alias,name,url,summary,specialties,history,year_established,rating,review_count,rating_distribution,not_recommended_review_count,review_counts_by_language,price_range,categories,phone,website,menu_url,address,city,state,postal_code,country,neighborhoods,timezone,latitude,longitude,service_areas,hours[],hours_today,is_open_now,is_closed,holidays,attributes,amenities,popular_items[],popular_items_omitted,review_highlights,health_inspections,question_count,questions,is_claimed,is_advertiser,verified_licenses,messaging_response_time,filtered_review_count,reviews[](page 1). - reviews —
business_id,alias,name,url,rating,review_count,rating_distribution,filtered_review_count,sort,rating_filter,start,page,has_next_page,count,reviews[](pos,review_id,url,rating,text,language,published_date,experience_date,author,author_url,author_photo,author_expertise,author_expertise_count,photos,videos,reactions,is_first_review,previous_review_count,owner_response,owner_response_date,owner_response_from,appreciated_by_owner).
{
"data": {
"term": "coffee",
"location": "Austin, TX",
"page": 1,
"start": 0,
"results_per_page": 10,
"total_results": 240,
"count": 10,
"businesses": [
{
"pos": 1,
"business_id": "kQ7pCn2m0Vx9",
"alias": "desnudo-coffee-austin-2",
"name": "Desnudo Coffee",
"rating": 4.5,
"review_count": 2153,
"price_range": "$",
"categories": ["Coffee & Tea"],
"city": "Austin",
"state": "TX",
"url": "https://www.yelp.com/biz/desnudo-coffee-austin-2"
}
]
},
"credits_used": 2,
"credits_remaining": 998
}
Guardrails
- Every call is 2 credits, including one that comes back empty - budget before paging.
- Never call
/reviewswithpage: 1./businessalready returned that page; the repeat is 2 credits for a duplicate. - Always send
location. Without it the answer is about wherever the request exited, and it can differ between two identical runs. - Never invent a
sortvalue. An unrecognised sort is ignored and you pay for default ranking. attributesis a passthrough, so an alias Yelp does not recognise silently disappears. Verify the filter took effect before telling the user it was applied.ratingon/reviewschangesfiltered_review_count, notreview_count. Quote the right one.- Yelp's recommendation software hides some reviews entirely. Those are never returned and are counted in
not_recommended_review_count- so the reviews you can read are not the whole picture, and an average you compute yourself will not match Yelp's. popular_itemscan arrive as stub shells; those rows are dropped andpopular_items_omittedflags it. If that flag istrue, do not claim the item list is complete.- Never fabricate business names, ratings, addresses, hours or review text. Only return what the API returned.
- Always include the business
urlso the user can verify.
Failure handling
400means an invalid or missing parameter -termwithoutlocationand nourl, asortoutside its enum, or more than 20 attributes. Fix and retry.401means the API key is invalid or missing. CheckSCAVIO_API_KEY.404means the business does not exist, or the review page is past the last one. Stop onhas_next_page: falserather than discovering the end with a billed 404.429means rate or usage limit exceeded. Wait before retrying. See rate limits.502/503mean upstream is temporarily unavailable - wait a few seconds and retry, up to a few times.- An empty search is usually the filters: drop
open_now, widenprice, or use a broaderterm. - If
SCAVIO_API_KEYis not set, prompt the user to export it before continuing.
SDKs
pip install scavio==0.15.0
from scavio import ScavioClient
client = ScavioClient() # reads SCAVIO_API_KEY
found = client.yelp.search(term="coffee", location="Austin, TX", sort="review_count")
alias = found["data"]["businesses"][0]["alias"]
biz = client.yelp.business(business_id=alias) # includes page 1 of reviews
more = client.yelp.reviews(business_id=alias, page=2) # start at 2, never 1
npm install scavio@0.15.0
import { Scavio } from "scavio";
const client = new Scavio(); // reads SCAVIO_API_KEY
const found = await client.yelp.search({ term: "coffee", location: "Austin, TX" });
const more = await client.yelp.reviews({ business_id: found.data.businesses[0].alias, page: 2 });
