Which API
There are two, and they do not overlap. The open data API at /api/open/v1 is public: what any profile already shows, readable by anyone, for nothing. The REST API at /api/v1 is yours: it acts on the businesses your organisation has claimed, and it is where anything that writes, or that needs data a profile does not show, lives.
| Open data API | REST API and insights | |
|---|---|---|
| Base path | /api/open/v1 | /api/v1 |
| Price | Free, every plan and no plan | Scale and Enterprise plans |
| Key | None | A trk_live_ key from your dashboard |
| Covers | Any business with a public profile | Only businesses your organisation manages, plus anonymous category benchmarks |
| Reads | Profile, score, counts, published reviews | The same, plus review source and status, invitations, monthly trends and benchmarks |
| Writes | Nothing | Invitations, bulk invitations, replies |
| Rate limit | 60 a minute per IP address | 600 a minute per key |
| Caching | Five minutes at the edge, CORS open | Not cached; server to server |
If you only need to show reviews or a score, use the open data API and skip the key. If you need to act on your own businesses or see how they compare, use the REST API.
Open data API, free, no key
Your reviews are yours, and a shopper’s reading of them should not depend on our website. Everything a public profile shows, the score, the counts and every published review, can be read as JSON without an account, a key or a plan. Use it to build your own widget, keep a copy of your reviews, or study a category.
GET/api/open/v1/businesses/{slug}
One business: name, domain, category, score, counts, rating distribution and reply rate. The slug is the part of the profile URL after /review/.
curl "https://thereviews.net/api/open/v1/businesses/acme.com"{
"data": {
"name": "Acme Kitchenware",
"slug": "acme.com",
"domain": "acme.com",
"category": { "slug": "kitchenware", "name": "Kitchenware" },
"score": 4.6,
"label": "Excellent",
"scored": true,
"ratingAvg": 4.71,
"reviewCount": 312,
"totalReviewCount": 312,
"verifiedReviewCount": 288,
"ratingDist": { "1": 4, "2": 6, "3": 18, "4": 61, "5": 223 },
"replyRate": 0.83,
"url": "https://thereviews.net/review/acme.com",
"updatedAt": "2026-09-12T18:04:12.000Z"
}
}As with the widget JSON, score and label are null until a business has 3 reviews. totalReviewCount adds the reviews from platforms the business has connected, which is the count the score is based on; reviewCount is reviews written here.
GET/api/open/v1/lookup?domain=acme.com
The same record, found by website instead of slug. Most slugs are the domain, but not every one is, so use this when all you have is the address of a shop. Send the bare domain; https://, www. and any path are stripped. An unknown domain returns 404.
GET/api/open/v1/businesses/{slug}/reviews?page=1
Published reviews, 20 a page, newest first. Held, hidden and removed reviews are never included. The reviewer is shown by display name only, exactly as on the profile; no email, IP address or invitation detail ever leaves the database through this API.
curl "https://thereviews.net/api/open/v1/businesses/acme.com/reviews?page=2"{
"business": { "name": "Acme Kitchenware", "slug": "acme.com", "url": "https://thereviews.net/review/acme.com" },
"data": [
{
"id": "rev_3f9a1c7e2b8d4a6f",
"rating": 5,
"title": "Pan arrived two days early",
"body": "Ordered the 28cm skillet on a Tuesday, it was on my hob by Thursday.",
"author": "Priya M.",
"createdAt": "2026-09-12T18:04:11.000Z",
"verified": true,
"reply": { "body": "Thanks Priya, glad it got there quickly.", "createdAt": "2026-09-13T08:30:00.000Z" },
"url": "https://thereviews.net/review/acme.com/rev_3f9a1c7e2b8d4a6f"
}
],
"page": 2, "perPage": 20, "total": 312, "totalPages": 16
}Limits and caching
Each IP address may make 60 requests a minute. Over that you get a 429 with a Retry-After header. Responses are cached at the edge for five minutes and send Access-Control-Allow-Origin: *, so you can call them from a browser. An unknown slug returns 404 with the same error shape as the REST API. Field names under /api/open/v1 will not be renamed or removed; new fields may appear, so ignore ones you do not recognise.
Terms, in plain words
- It is free, for any purpose, commercial or not.
- Wherever you show a score or a review, link to the business’s profile on TheReviews (the
urlfield) so a reader can check it. - Show reviews as written. Shorten a long one if you must, but do not change the words, the rating or the author.
- Do not suggest that TheReviews, or the reviewer, endorses you or your product.
- Keep to the rate limit. If you need the whole platform, write to us rather than crawling it.
Business owners can also download every published review as a CSV from the Reviews page of the dashboard, on every plan including Free.
Authentication
The REST API is for your own organisation’s data and needs a key. Create one from your dashboard under Settings, then API keys. Keys look like trk_live_ followed by 40 characters, and the full value is shown once. We store a hash, so if you lose it you will need to revoke it and create another.
Send the key as a bearer token on every request:
curl https://thereviews.net/api/v1/me \
-H "Authorization: Bearer trk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Keys carry read and write scopes. A key with only read can list businesses, reviews and invitations but cannot queue invitations or post replies. Every request is also tied to the organisation that created the key, so you only ever see the businesses your organisation has claimed.
Rate limits
Each key may make 600 requests a minute. The window is fixed, not sliding. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a Unix timestamp). When you go over, you get a 429 with a Retry-After header. Bulk invitation calls count as one request, so use them for anything above a handful of customers.
Errors and pagination
Errors always use the same shape, with a stable machine-readable code and a sentence for humans:
{
"error": {
"code": "not_found",
"message": "No business with that id belongs to your organisation."
}
}| Status | Code | When |
|---|---|---|
| 400 | invalid_json, invalid_parameter, invalid_email | The body or query string could not be read |
| 401 | missing_api_key, invalid_api_key | No key, an unknown key, or a revoked one |
| 402 | limit_reached | Invitation allowance used up for the period |
| 403 | plan_required, plan_inactive, insufficient_scope | The plan or key does not allow the call |
| 404 | not_found | The id exists nowhere in your organisation |
| 409 | duplicate, not_published | Repeat invitation reference, or replying to a review that is not public |
| 422 | validation_error, reply_failed | The body was JSON but a field failed validation |
| 429 | rate_limited | Over the per-minute limit |
List endpoints accept page (from 1) and perPage (default 25, maximum 100) and return the same envelope:
{ "data": [ ... ], "page": 1, "perPage": 25, "total": 312, "totalPages": 13 }Single resources are wrapped in data too. Timestamps are ISO 8601 in UTC.
Businesses
GET/api/v1/businesses
Every business your organisation manages, with the current score and counts. Sorted by name.
curl "https://thereviews.net/api/v1/businesses" \
-H "Authorization: Bearer $THEREVIEWS_KEY"{
"data": [
{
"id": "biz_8k2m1p0q7r3s5t9u",
"slug": "acme.com",
"name": "Acme Kitchenware",
"domain": "acme.com",
"website": "https://acme.com",
"country": "GB",
"city": "Leeds",
"verificationStatus": "verified",
"verifiedAt": "2026-03-04T10:12:00.000Z",
"score": 4.6,
"label": "Excellent",
"scored": true,
"ratingAvg": 4.71,
"reviewCount": 312,
"verifiedReviewCount": 288,
"ratingDist": { "1": 4, "2": 6, "3": 18, "4": 61, "5": 223 },
"replyRate": 0.83,
"url": "https://thereviews.net/review/acme.com",
"createdAt": "2026-01-15T09:00:00.000Z"
}
],
"page": 1, "perPage": 25, "total": 1, "totalPages": 1
}Reviews
GET/api/v1/businesses/{id}/reviews
Published reviews for one business, newest first. Hidden, pending and removed reviews are never returned by the API.
| Query | Meaning |
|---|---|
rating | Only reviews with this star rating, 1 to 5 |
verified | true for reviews tied to a purchase, false for organic |
since | ISO date. Only reviews created on or after it. Handy for polling. |
page, perPage | Pagination as above |
curl "https://thereviews.net/api/v1/businesses/biz_8k2m1p0q7r3s5t9u/reviews?verified=true&since=2026-09-01T00:00:00Z&perPage=50" \
-H "Authorization: Bearer $THEREVIEWS_KEY"{
"data": [
{
"id": "rev_3f9a1c7e2b8d4a6f",
"businessId": "biz_8k2m1p0q7r3s5t9u",
"rating": 5,
"title": "Pan arrived two days early",
"body": "Ordered the 28cm skillet on a Tuesday, it was on my hob by Thursday. Heavy, even heat, no hot spots so far.",
"author": "Priya M.",
"verified": true,
"source": "invitation",
"status": "published",
"language": "en",
"experienceDate": "2026-09-10",
"helpfulCount": 3,
"createdAt": "2026-09-12T18:04:11.000Z",
"updatedAt": "2026-09-12T18:04:11.000Z",
"editedAt": null,
"url": "https://thereviews.net/review/acme.com/rev_3f9a1c7e2b8d4a6f",
"reply": { "id": "rpl_...", "body": "Thanks Priya, glad it got there quickly.", "createdAt": "2026-09-13T08:30:00.000Z" }
}
],
"page": 1, "perPage": 50, "total": 288, "totalPages": 6
}GET/api/v1/reviews/{id}
One review by id, same shape, wrapped in data. Returns 404 for reviews on businesses outside your organisation and for removed reviews.
Invitations
An invitation is an email, text or WhatsApp message asking a real customer to review a purchase. Reviews that come back through it are marked Verified and carry full weight in the score. Invitations count against your plan allowance for the month and expire after 30 days if unused.
POST/api/v1/invitations
| Field | Type | Notes |
|---|---|---|
businessId | string, required | A business your organisation manages |
email | string, required | The customer’s address |
name | string | Used in the greeting |
reference | string | Order number or similar. One invitation per email and reference; repeats return 409. |
phone | string | The customer’s mobile from the order, ideally in international format such as +447700900123 |
channel | email, sms or whatsapp | Defaults to your preferred channel, falling back to email. Texts need a paid plan, a phone and a reference; asking for one without them returns invalid_phone, reference_required, phone_opted_out or messaging_not_available. |
sendAt | ISO datetime | Delay the invitation, for example until the parcel has landed. Defaults to now. Texts never go out between 21:00 and 08:00 in the business’s time zone and wait for the morning. |
delivery | email or link | link sends no email. The response carries inviteUrl, once, for you to send from your own email tool. It is marked sent straight away and still counts against the allowance. |
curl -X POST "https://thereviews.net/api/v1/invitations" \
-H "Authorization: Bearer $THEREVIEWS_KEY" \
-H "Content-Type: application/json" \
-d '{
"businessId": "biz_8k2m1p0q7r3s5t9u",
"email": "priya@example.com",
"name": "Priya",
"reference": "#10482",
"sendAt": "2026-09-20T09:00:00Z"
}'{
"data": {
"id": "inv_7d2c9e1b4a8f0c3e",
"businessId": "biz_8k2m1p0q7r3s5t9u",
"email": "priya@example.com",
"name": "Priya",
"reference": "#10482",
"status": "queued",
"source": "api",
"sendAt": "2026-09-20T09:00:00.000Z",
"sentAt": null,
"openedAt": null,
"reviewedAt": null,
"expiresAt": "2026-10-20T09:00:00.000Z",
"createdAt": "2026-09-18T14:02:00.000Z"
}
}POST/api/v1/invitations/bulk
Up to 500 invitations in one call for a single business. Each row is queued on its own and the response tells you what happened to every one. If the allowance runs out part way through, the remaining rows come back as limit_reached and nothing else is queued.
curl -X POST "https://thereviews.net/api/v1/invitations/bulk" \
-H "Authorization: Bearer $THEREVIEWS_KEY" \
-H "Content-Type: application/json" \
-d '{
"businessId": "biz_8k2m1p0q7r3s5t9u",
"invitations": [
{ "email": "priya@example.com", "name": "Priya", "reference": "#10482" },
{ "email": "tom@example.com", "reference": "#10483", "sendAt": "2026-09-21T09:00:00Z" }
]
}'{
"data": {
"queued": 1,
"failed": 1,
"results": [
{ "index": 0, "email": "priya@example.com", "ok": false, "error": "duplicate" },
{ "index": 1, "email": "tom@example.com", "ok": true, "invitationId": "inv_1a2b3c4d5e6f7a8b" }
]
}
}GET/api/v1/invitations
Your organisation’s invitations, newest first. Filter with businessId and status (queued, sent, opened, reviewed, expired, bounced).
Replies
POST/api/v1/replies
Post your organisation’s public reply to a review. One reply per review; posting again replaces it. The reviewer is emailed. Body must be between 10 and 3,000 characters. The reply is attributed to the person who created the API key.
curl -X POST "https://thereviews.net/api/v1/replies" \
-H "Authorization: Bearer $THEREVIEWS_KEY" \
-H "Content-Type: application/json" \
-d '{ "reviewId": "rev_3f9a1c7e2b8d4a6f", "body": "Thanks Priya, glad the skillet got there quickly. Season it once before the first fry and it will last decades." }'Your organisation
GET/api/v1/me
The organisation behind the key, its plan, the limits that apply and how much of them you have used. Useful for a health check when you first wire things up.
{
"data": {
"organisation": { "id": "org_5c1d8e2f9a3b7c0d", "name": "Acme Kitchenware Ltd", "createdAt": "2026-01-15T09:00:00.000Z" },
"plan": { "id": "scale", "name": "Scale", "status": "active", "billingInterval": "yearly", "currentPeriodEnd": "2027-01-15T09:00:00.000Z" },
"limits": { "invitesPerMonth": 5000, "seats": 10, "locations": 10, "api": true, "webhooks": true, "integrations": true, "widgets": "all", "rateLimitPerMinute": 600 },
"usage": { "invitesUsedThisPeriod": 1240, "invitesQueuedThisPeriod": 1310, "invitesRemaining": 3760, "periodStart": "2026-09-01T00:00:00.000Z", "businesses": 3, "publishedReviews": 812, "activeWebhooks": 2, "activeApiKeys": 1 },
"key": { "id": "key_...", "name": "Warehouse system", "prefix": "trk_live_a1b", "scopes": ["read", "write"], "createdAt": "...", "lastUsedAt": "..." }
}
}Insights
Two read-only endpoints for seeing how your businesses are doing over time and against others in their category. They count customer reviews written on TheReviews: editor’s reviews and reviews from connected platforms are left out, except that the score is the published score, which blends connected platforms in. Part of the REST API, so they need a key and the Scale or Enterprise plan.
GET/api/v1/insights/benchmarks?category={slug}
Figures across every business in a category and its sub-categories, as the 25th percentile, median and 75th percentile. Leave out category for the whole platform. The slug is the last part of a /categories/ URL; an unknown one returns 404.
| Field | Meaning |
|---|---|
score | The TheReviews score, 1 to 5 |
rating | Average star rating of customer reviews written here |
replyRate | Share of reviews with a reply from the business, 0 to 1 |
verifiedShare | Share of reviews tied to a verified purchase, 0 to 1 |
reviewsPerMonth | Review velocity: new reviews a month, averaged over the last 90 days |
replyHours | Each business’s median hours to reply over the last 12 months, then spread across businesses. null when fewer than 5 businesses have one. |
distribution | Share of each star rating across all reviews in the sample in the last 12 months |
yourBusinesses | Each of your businesses in the category, with its own values and its percentile (the share of the sample below it) on each measure |
Minimum samples: a business counts once it has 5 published customer reviews, and a category needs 10 such businesses before any figure is returned. Below that, sample.sufficient is false and metrics and the percentiles are null, so a small category can never be read back to one business. Benchmarks never name another business.
curl "https://thereviews.net/api/v1/insights/benchmarks?category=kitchenware" -H "Authorization: Bearer $THEREVIEWS_KEY"{
"data": {
"category": { "slug": "kitchenware", "name": "Kitchenware" },
"generatedAt": "2026-10-17T09:00:00.000Z",
"sample": { "businesses": 64, "reviews": 9120, "minimumBusinesses": 10, "minimumReviewsPerBusiness": 5, "sufficient": true },
"metrics": {
"score": { "p25": 3.6, "median": 4.1, "p75": 4.4 },
"rating": { "p25": 3.82, "median": 4.3, "p75": 4.61 },
"replyRate": { "p25": 0.12, "median": 0.41, "p75": 0.77 },
"verifiedShare": { "p25": 0.38, "median": 0.62, "p75": 0.81 },
"reviewsPerMonth": { "p25": 2.3, "median": 6.8, "p75": 14.1, "windowDays": 90 },
"replyHours": { "p25": 6.5, "median": 19, "p75": 52, "businesses": 41 },
"distribution": { "share": { "1": 0.06, "2": 0.03, "3": 0.06, "4": 0.17, "5": 0.68 }, "reviews": 5410, "windowDays": 365 }
},
"yourBusinesses": [
{
"businessId": "biz_8k2m1p0q7r3s5t9u",
"inSample": true,
"reviews": 312,
"values": { "score": 4.6, "rating": 4.71, "replyRate": 0.83, "verifiedShare": 0.92, "reviewsPerMonth": 11.3, "replyHours": 7.5 },
"percentiles": { "score": 88, "rating": 79, "replyRate": 81, "verifiedShare": 93, "reviewsPerMonth": 67 }
}
]
}
}GET/api/v1/insights/business/{id}/trends?months=12
One of your businesses month by month, oldest first, ending with the current month to date (complete is false for it). months runs from 1 to 36 and defaults to 12; months are calendar months in UTC. A business outside your organisation returns 404.
| Field | Meaning |
|---|---|
reviews, verified | Reviews written that month, and how many were verified purchases |
averageRating, ratingMix | The month’s average stars and the count at each star. null average in a month with no reviews. |
replied, replyRate, medianReplyHours | Of that month’s reviews, how many have a reply, and the median hours from review to reply |
score, scored | The score as at the end of the month, from the reviews written by then. Connected platforms count at their current ratings, because we do not keep their history. |
reviewsToDate | Reviews written up to the end of the month |
Counts follow what the profile shows today: a review removed since is gone from every month, including the score history.
curl "https://thereviews.net/api/v1/insights/business/biz_8k2m1p0q7r3s5t9u/trends?months=3" -H "Authorization: Bearer $THEREVIEWS_KEY"{
"data": {
"business": { "id": "biz_8k2m1p0q7r3s5t9u", "slug": "acme.com", "name": "Acme Kitchenware" },
"from": "2026-08-01T00:00:00.000Z",
"to": "2026-10-17T09:00:00.000Z",
"months": [
{
"month": "2026-08",
"complete": true,
"reviews": 14,
"verified": 13,
"averageRating": 4.64,
"ratingMix": { "1": 0, "2": 1, "3": 0, "4": 2, "5": 11 },
"replied": 12,
"replyRate": 0.86,
"medianReplyHours": 6.5,
"score": 4.6,
"scored": true,
"reviewsToDate": 290
}
]
}
}Webhooks
Add an endpoint in your dashboard under Settings, then Webhooks, choose the events you want and we give you a signing secret. We POST JSON to the URL and expect a 2xx within 10 seconds. Anything else, including a timeout, is retried with backoff at 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours. After six failed attempts the delivery is dropped, and after six consecutive failures across deliveries the webhook is switched off and you are told in the dashboard.
Events
| Event | Fires when | Payload includes |
|---|---|---|
review.created | A review is published, including after a moderator clears a held one | business, review |
review.updated | The author edits it, or its status changes without removal | business, review, reply if any |
review.removed | A review is removed by moderation or deleted by its author | business, review |
reply.created | Your organisation replies | business, review, reply |
invitation.sent | A queued invitation email goes out | business, invitation (email masked) |
business.verified | A claim is approved | business |
Headers and payload
| Header | Value |
|---|---|
X-TheReviews-Event | The event name, for routing before you parse the body |
X-TheReviews-Delivery | A unique id per delivery. Retries reuse it, so use it to de-duplicate. |
X-TheReviews-Signature | sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with your secret |
{
"id": "whd_2e7b1c9a4d8f0e3a",
"event": "review.created",
"createdAt": "2026-09-12T18:04:12.000Z",
"data": {
"business": { "id": "biz_8k2m1p0q7r3s5t9u", "slug": "acme.com", "name": "Acme Kitchenware", "url": "https://thereviews.net/review/acme.com" },
"review": {
"id": "rev_3f9a1c7e2b8d4a6f",
"rating": 5,
"title": "Pan arrived two days early",
"body": "Ordered the 28cm skillet on a Tuesday...",
"author": "Priya M.",
"verified": true,
"source": "invitation",
"status": "published",
"experienceDate": "2026-09-10",
"createdAt": "2026-09-12T18:04:11.000Z",
"url": "https://thereviews.net/review/acme.com/rev_3f9a1c7e2b8d4a6f"
}
}
}Verifying the signature
Compute the HMAC over the exact bytes you received, before any JSON parsing, and compare in constant time. In Node:
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(request) {
const raw = await request.text();
const header = request.headers.get("x-thereviews-signature") ?? "";
const expected = "sha256=" + createHmac("sha256", process.env.THEREVIEWS_WEBHOOK_SECRET).update(raw).digest("hex");
const a = Buffer.from(header);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(raw);
const deliveryId = request.headers.get("x-thereviews-delivery");
// Store deliveryId and skip if you have seen it before, then act on event.data.
return new Response("ok");
}Subscribing from code
Automation tools, and anything else that wants to switch a webhook on and off for itself, can manage them with an API key that has the write scope. These are what the TheReviews apps for Zapier, Make and n8n call when a trigger is switched on and off.
POST/api/v1/webhooks
Body { "url": "https://...", "events": ["review.created"] }. The response includes the secret, once. Subscribing the same URL again updates its events and returns the same webhook. review.resolved fires when, after your reply, the reviewer says the problem was sorted; its payload carries resolution and resolutionAt.
DELETE/api/v1/webhooks/{id}
GET/api/v1/webhooks
GET/api/v1/webhooks/samples?event=review.created
Up to three recent payloads for an event, built from your own data in exactly the delivery shape. Nothing is sent.
Respond quickly and do the real work afterwards. If your handler takes longer than 10 seconds we count it as a failure and retry, which means you will see the same delivery id again.
Inbound integrations
Shopify, WooCommerce and Zapier can queue invitations without any code on your side. Each integration in your dashboard has its own URL and secret, and a delay in days (default 7) so the email arrives after the goods do. Orders that carry no email address are ignored. A repeat order number for the same customer is ignored too, so it is safe to point several webhook topics at the same URL.
Shopify
In Shopify admin go to Settings, Notifications, Webhooks and create a webhook for orders/paid or orders/fulfilled in JSON format pointing at your integration URL. Shopify signs the body with your store’s webhook signing secret in X-Shopify-Hmac-Sha256; paste that secret into the integration as the Shopify secret so we can check it.
POST https://thereviews.net/api/integrations/shopify/{integrationId}
X-Shopify-Hmac-Sha256: <base64 HMAC-SHA256 of the raw body>WooCommerce
In WooCommerce go to Settings, Advanced, Webhooks and add one for Order updated or Order completed. Set the Secret field to the integration secret from your dashboard. WooCommerce sends it as a base64 HMAC in X-WC-Webhook-Signature. The first save sends a test ping, which we accept and ignore.
POST https://thereviews.net/api/integrations/woocommerce/{integrationId}
X-WC-Webhook-Signature: <base64 HMAC-SHA256 of the raw body>Zapier and anything else
Use a Webhooks by Zapier POST action, or any tool that can send JSON, with the integration secret as a bearer token:
curl -X POST "https://thereviews.net/api/integrations/zapier/{integrationId}" \
-H "Authorization: Bearer <integration secret>" \
-H "Content-Type: application/json" \
-d '{ "email": "priya@example.com", "name": "Priya", "reference": "#10482" }'We reply 200 to every correctly signed request, even duplicates, so the sender stops retrying. A bad signature gets 401; an unknown or paused integration gets 404.
Widgets
Paste one script tag where you want the widget. It creates an iframe that sizes itself to its content, so it works inside any layout. You can drop several on one page with different variants.
<div class="thereviews-widget"
data-business="acme.com"
data-variant="carousel"
data-theme="light">
<a href="https://thereviews.net/review/acme.com">Read our reviews on TheReviews.net</a>
</div>
<script async src="https://thereviews.net/widget.js"></script>The link inside the container is part of your page, so it works without JavaScript and search engines treat it as a normal link to your profile. The widget renders above it and the link always shows as a small caption underneath, even if your stylesheet hides it. A badge placed without a link gets one added: Read our reviews on TheReviews.net. Load widget.js once per page, however many widgets you place. The older single-tag form (<script data-business="acme.com">) still works.
| Attribute | Values |
|---|---|
data-business | Your profile slug, the part after /review/. Required. |
data-variant | badge (score pill, stars, count), mini (stars and “Rated 4.6 on TheReviews”), list (your latest five reviews), carousel (ten reviews, one at a time, auto-advancing every six seconds and pausing on hover) |
data-theme | light or dark. The page behind the widget is transparent either way. |
data-height | Initial height in pixels before the first resize message, if you want to avoid a layout shift |
data-mode | inline renders the widget straight into your page instead of an iframe, using your fonts, with a normal link back to your profile that search engines can follow. Works with every variant. |
Every variant links to your public profile with utm_source=widget so you can see the clicks in your analytics. The Free plan includes the badge and mini variants; list and carousel come with Starter and above.
Links that count
Links inside an iframe do not count as links from your site. If you want search engines to see a link from your page to your TheReviews profile, use data-mode="inline", or paste one of these plain snippets anywhere in your HTML:
<!-- Text link -->
<a href="https://thereviews.net/review/acme.com">Read our reviews on TheReviews.net</a>
<!-- Live score badge (SVG, always current) -->
<a href="https://thereviews.net/review/acme.com">
<img src="https://thereviews.net/api/badge/acme.com.svg" alt="Acme reviews on TheReviews.net" height="48">
</a>
<a href="https://thereviews.net/review/acme.com">Read our reviews on TheReviews.net</a>
<!-- On a dark site: transparent, takes your background -->
<img src="https://thereviews.net/api/badge/acme.com.svg?theme=dark" alt="Acme reviews on TheReviews.net" height="48">Building your own
If you would rather render reviews in your own markup, the JSON behind the widget is public, sends Access-Control-Allow-Origin: * and is cached for five minutes:
curl "https://thereviews.net/api/widget/acme.com?limit=5"{
"name": "Acme Kitchenware",
"slug": "acme.com",
"score": 4.6,
"label": "Excellent",
"scored": true,
"ratingAvg": 4.71,
"reviewCount": 312,
"verifiedReviewCount": 288,
"stars": 4.5,
"ratingDist": { "1": 4, "2": 6, "3": 18, "4": 61, "5": 223 },
"url": "https://thereviews.net/review/acme.com?utm_source=widget&utm_medium=json",
"reviews": [
{ "id": "rev_...", "rating": 5, "title": "Pan arrived two days early", "body": "...", "author": "Priya M.", "createdAt": "2026-09-12T18:04:11.000Z", "verified": true, "url": "..." }
]
}A business with fewer than 3 reviews has no score yet. In that case scored is false, score and label are null, and stars follows ratingAvg. Show the average and the review count rather than a score. The REST API always returns the stored score and adds the same scored flag.
SVG badge
A static image for email signatures, README files and places that will not run a script. It shows the score, stars, the count and the TheReviews mark, and is cached for five minutes.
<a href="https://thereviews.net/review/acme.com?utm_source=badge">
<img src="https://thereviews.net/api/badge/acme.com.svg" alt="Acme Kitchenware is rated 4.6 on TheReviews" height="48">
</a>Add ?theme=dark for a dark background. In Markdown: .
Rich snippets
Your public profile at /review/{slug} already carries schema.org AggregateRating and Review markup as JSON-LD, so search engines can show your stars against the profile. You do not need to add anything for that. The rating in it is the average of customer reviews written on TheReviews: ratings from connected sites and editor’s reviews are left out, as Google’s review snippet rules require, and the page says so wherever the figure differs from the all-reviews average.
Do not copy that rating into Organization or LocalBusiness markup on your own site. Google treats a business marking up reviews of itself as self-serving and will not show stars for it, wherever the reviews come from. We used to suggest it here; we were wrong, and the dashboard no longer offers the snippet.
Product markup on your own product pages is different: Google allows ratings there, as long as the rating and the reviews are visible on that same page and come from your customers. If you render product ratings into your own HTML from /api/widget/{slug}/products/{id}, mark up exactly what you show and refresh it when the JSON changes. Ratings inside an iframe are not part of your page, so they cannot be marked up.
Questions
Something missing, or a payload that does not match this page? Write to email@thereviews.net with the delivery id or request path and we will look at it. Plan details are on the pricing page, and how the score is calculated is on the trust page.