thereviews

Developers

API, webhooks and widgets

Everything a developer needs to pull reviews into your own systems, send invitations from your order flow, benchmark your businesses and show your score on your site. The open data API is free and needs no key: anyone can read any public profile with it. The REST API, which works on your own businesses, sends invitations, posts replies and includes the insights endpoints, is available on the Scale and Enterprise plans. Widgets and the badge are free for every claimed business.

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 APIREST API and insights
Base path/api/open/v1/api/v1
PriceFree, every plan and no planScale and Enterprise plans
KeyNoneA trk_live_ key from your dashboard
CoversAny business with a public profileOnly businesses your organisation manages, plus anonymous category benchmarks
ReadsProfile, score, counts, published reviewsThe same, plus review source and status, invitations, monthly trends and benchmarks
WritesNothingInvitations, bulk invitations, replies
Rate limit60 a minute per IP address600 a minute per key
CachingFive minutes at the edge, CORS openNot 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 url field) 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."
  }
}
StatusCodeWhen
400invalid_json, invalid_parameter, invalid_emailThe body or query string could not be read
401missing_api_key, invalid_api_keyNo key, an unknown key, or a revoked one
402limit_reachedInvitation allowance used up for the period
403plan_required, plan_inactive, insufficient_scopeThe plan or key does not allow the call
404not_foundThe id exists nowhere in your organisation
409duplicate, not_publishedRepeat invitation reference, or replying to a review that is not public
422validation_error, reply_failedThe body was JSON but a field failed validation
429rate_limitedOver 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.

QueryMeaning
ratingOnly reviews with this star rating, 1 to 5
verifiedtrue for reviews tied to a purchase, false for organic
sinceISO date. Only reviews created on or after it. Handy for polling.
page, perPagePagination 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

FieldTypeNotes
businessIdstring, requiredA business your organisation manages
emailstring, requiredThe customer’s address
namestringUsed in the greeting
referencestringOrder number or similar. One invitation per email and reference; repeats return 409.
phonestringThe customer’s mobile from the order, ideally in international format such as +447700900123
channelemail, sms or whatsappDefaults 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.
sendAtISO datetimeDelay 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.
deliveryemail or linklink 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.

FieldMeaning
scoreThe TheReviews score, 1 to 5
ratingAverage star rating of customer reviews written here
replyRateShare of reviews with a reply from the business, 0 to 1
verifiedShareShare of reviews tied to a verified purchase, 0 to 1
reviewsPerMonthReview velocity: new reviews a month, averaged over the last 90 days
replyHoursEach business’s median hours to reply over the last 12 months, then spread across businesses. null when fewer than 5 businesses have one.
distributionShare of each star rating across all reviews in the sample in the last 12 months
yourBusinessesEach 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.

FieldMeaning
reviews, verifiedReviews written that month, and how many were verified purchases
averageRating, ratingMixThe month’s average stars and the count at each star. null average in a month with no reviews.
replied, replyRate, medianReplyHoursOf that month’s reviews, how many have a reply, and the median hours from review to reply
score, scoredThe 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.
reviewsToDateReviews 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

EventFires whenPayload includes
review.createdA review is published, including after a moderator clears a held onebusiness, review
review.updatedThe author edits it, or its status changes without removalbusiness, review, reply if any
review.removedA review is removed by moderation or deleted by its authorbusiness, review
reply.createdYour organisation repliesbusiness, review, reply
invitation.sentA queued invitation email goes outbusiness, invitation (email masked)
business.verifiedA claim is approvedbusiness

Headers and payload

HeaderValue
X-TheReviews-EventThe event name, for routing before you parse the body
X-TheReviews-DeliveryA unique id per delivery. Retries reuse it, so use it to de-duplicate.
X-TheReviews-Signaturesha256= 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.

AttributeValues
data-businessYour profile slug, the part after /review/. Required.
data-variantbadge (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-themelight or dark. The page behind the widget is transparent either way.
data-heightInitial height in pixels before the first resize message, if you want to avoid a layout shift
data-modeinline 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: ![Rated on TheReviews](https://thereviews.net/api/badge/acme.com.svg).

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.