thereviews

Developers

API, webhooks and widgets

Everything a developer needs to pull reviews into your own systems, send invitations from your order flow, and show your score on your site. The API is available on the Scale and Enterprise plans. Widgets and the badge are free for every claimed business.

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."
  }
}
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",
      "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 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.
sendAtISO datetimeDelay 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

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");
}

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.

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 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: ![Rated on TheReviews](https://thereviews.net/api/badge/acme.com.svg).

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.