Authentication
Create an API key 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",
"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 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. |
sendAt | ISO datetime | Delay the email, for example until the parcel has landed. Defaults to now. |
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": "..." }
}
}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");
}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">Acme reviews on TheReviews</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 stays as a small caption underneath. 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 Growth 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</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" width="220" height="48">
</a>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",
"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": "..." }
]
}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 includes 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.
If you want stars on your own pages, add an AggregateRating to your Organization or Product markup using the numbers from /api/widget/{slug}, and keep the ratingCount honest by refreshing it when the JSON changes. Google’s guidelines ask that self-serving ratings come from a third party that the visitor can check; linking the markup to your TheReviews profile satisfies that.
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Acme Kitchenware",
"url": "https://acme.com",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": 4.6,
"bestRating": 5,
"ratingCount": 312,
"url": "https://thereviews.net/review/acme.com"
}
}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.