A small, honest REST API.
The Starvo Public API is a thin, read-only REST surface for pulling your reviews and stats into a website, BI tool, or internal dashboard. Two endpoints, keyed access, predictable response shape, no surprises.
Overview
- Base URL:
https://starvo.app - Plan requirement: Pro Max. API keys cannot be created on lower plans.
- Endpoints:
GET /api/public/reviewsandGET /api/public/stats - Auth: the
X-API-Keyheader, using a key of the formsk_live_...(anAuthorization: Bearerheader is also accepted) - Rate limit: 1,000 requests per hour per API key
- Response shape: always
{ success, data, error } - Versioning: the URL path is the version contract. Breaking changes will introduce
/api/public/v2/...
Getting an API key
- 1. Make sure your business is on the Pro Max plan (Dashboard → Billing).
- 2. Open your dashboard and find the “Advanced” section, which holds API Keys.
- 3. Click Create key. Name it (e.g. “production website”).
- 4. Copy the key immediately. It is shown once. Starvo stores only a SHA-256 hash; we cannot recover a lost key.
- 5. If you lose a key, delete it and create a new one. Treat keys like passwords.
sk_live_. If a key you have starts with anything else, it is invalid (or test-mode, which is not exposed publicly yet).Authentication
Pass the key in the X-API-Key header (this is the canonical form):
X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxAn Authorization: Bearer sk_live_... header is also accepted as a legacy fallback, so existing integrations keep working — but X-API-Key is the documented, recommended header.
The key is scoped to a single Starvo account and authorizes every business that account owns. All endpoints automatically filter results to the account that owns the key.
Rate limits
- 1,000 requests per hour, per API key. Rolling window.
- Over-quota requests return
HTTP 429witherror: "Rate limit exceeded. Max 1000 requests per hour per API key." - If you have a use case that legitimately needs more, email support@starvo.app.
Response envelope
Every endpoint returns the same envelope:
{
"success": true,
"data": <object | array | null>,
"error": <string | null>
}On success, success is true, data contains the payload, and error is null. On failure, the reverse — success is false, data is null, and error contains a human-readable message.
Errors
| HTTP | Error string | When |
|---|---|---|
| 401 | Missing or invalid API key. Send your key as the X-API-Key header… | No X-API-Key (or Bearer) header, or wrong key prefix. |
| 401 | Invalid API key | Key not found or revoked. |
| 403 | UPGRADE_REQUIRED: Public API access requires the Pro Max plan. | Business is below Pro Max. |
| 429 | Rate limit exceeded. Max 1000 requests per hour per API key. | Over 1,000 requests in the last hour. |
| 500 | Failed to fetch reviews / etc. | Database error — rare; retry once. |
GET /api/public/reviews
Returns published reviews for your business (private feedback is excluded). Most recent first. Paginated.
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
limit | int | 50 | 1–100. Clamped if outside that range. |
page | int | 1 | 1-indexed. Use page + limit for pagination. |
Response shape
{
"success": true,
"data": [
{
"rating": 5,
"comment": "Great food, great service.",
"created_at": "2026-04-12T18:21:09.000Z"
},
{
"rating": 4,
"comment": null,
"created_at": "2026-04-11T20:04:00.000Z"
}
],
"error": null
}Comments are sanitized server-side (HTML stripped, truncated at 1,000 characters). Customer email addresses are never exposed — only the rating, the comment, and the timestamp.
GET /api/public/stats
Returns aggregated review statistics for your business.
Response shape
{
"success": true,
"data": {
"total_reviews": 1247,
"average_rating": 4.6,
"unresolved_count": 12,
"rating_breakdown": {
"1": 28,
"2": 41,
"3": 73,
"4": 318,
"5": 787
}
},
"error": null
}average_rating is rounded to one decimal place. unresolved_count is the number of public reviews not yet marked resolved. rating_breakdown is keyed by star value (1–5). Private and deleted reviews are excluded. Same business-scoping rules as the reviews endpoint.
Examples
curl
# Fetch the 10 most recent reviews
curl https://starvo.app/api/public/reviews?limit=10 \
-H "X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Fetch stats
curl https://starvo.app/api/public/stats \
-H "X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Node.js (fetch)
const KEY = process.env.STARVO_API_KEY;
async function fetchReviews(limit = 50, page = 1) {
const res = await fetch(
`https://starvo.app/api/public/reviews?limit=${limit}&page=${page}`,
{ headers: { 'X-API-Key': KEY } }
);
const body = await res.json();
if (!body.success) throw new Error(body.error);
return body.data;
}
async function fetchStats() {
const res = await fetch(
'https://starvo.app/api/public/stats',
{ headers: { 'X-API-Key': KEY } }
);
const body = await res.json();
if (!body.success) throw new Error(body.error);
return body.data;
}Python (requests)
import os, requests
KEY = os.environ["STARVO_API_KEY"]
HEAD = {"X-API-Key": KEY}
def fetch_reviews(limit=50, page=1):
r = requests.get(
"https://starvo.app/api/public/reviews",
params={"limit": limit, "page": page},
headers=HEAD,
timeout=10,
)
body = r.json()
if not body["success"]:
raise RuntimeError(body["error"])
return body["data"]
def fetch_stats():
r = requests.get(
"https://starvo.app/api/public/stats",
headers=HEAD,
timeout=10,
)
body = r.json()
if not body["success"]:
raise RuntimeError(body["error"])
return body["data"]Changelog
- v1 (current) — first public release.
/api/public/reviewsand/api/public/stats. - Coming: per-location filtering, sentiment + topic in the reviews payload, webhooks for new-review events.
Want a specific addition? Email support@starvo.app. API changes follow the rule: never break an existing endpoint shape. New fields are added, old fields are not removed. If we have to remove a field, it gets a new versioned URL.