# Judge.me

> Send Judge.me product and store reviews to Dimension Labs with the universal API.

This brings in the reviews in your Judge.me store, product reviews and store reviews: each review as a customer message, with its rating, product and verified status. One conversation per review. You run it on your side: a small script pulls from the Judge.me API and posts each message to the [universal API](/api/tracker).

## What you need

- A Judge.me store, your **Private API Token** and your **shop domain** (in `example.myshopify.com` format).
- A Dimension Labs API key for a new **Universal API** source. See [Your API key](/api/tracker#your-api-key).

## 1. Get API access in Judge.me

Judge.me's steps are in [Using Judge.me API](https://judge.me/help/en/articles/8409180-using-judge-me-api).

1. In your Judge.me admin, go to **Settings > Integrations**.
2. Click **View API tokens** at the top right. Copy the **Private API Token** and your **Shop domain**.
3. Send the token in the `X-Api-Token` header, with `shop_domain` as a query parameter. The private token grants read and write access to your data, so use it on a server only. Judge.me says you can't regenerate it yourself: if it is ever exposed, contact Judge.me support for a new one.

## 2. Pull new reviews

```bash
curl -H "X-Api-Token: $JUDGEME_PRIVATE_TOKEN" \
  "https://api.judge.me/api/v1/reviews?shop_domain=example.myshopify.com&per_page=100&page=1"
```

- With no `product_id`, the endpoint returns all product and store reviews of the store.
- The reviews endpoints return raw review information, which can include **unpublished reviews**. `curated` says whether a review is published (`ok`), not published (`spam`) or waiting (`not-yet`).
- **Incremental:** the endpoint has no "updated since" filter, so the code pages through every review and sends those whose `updated_at` is at or after the cursor.
- **Pagination:** `page` and `per_page` (up to 100).
- Judge.me's help center says the response doesn't include replies to reviews, so only the review is sent.
- **Rate limits:** Judge.me's API documentation doesn't state one. The code waits and retries on `429` and `5xx`.

## 3. Map the fields

Each review is one incoming message under its own `sessionId`.

| Dimension Labs | Judge.me | Notes |
|---|---|---|
| `text` | `title` and `body` | Join the title and body with a line break. |
| `userId` | `reviewer.id` | Judge.me's internal reviewer id, prefixed `judge-me-reviewer-`. Don't send `reviewer.email`, `reviewer.name` or `reviewer.phone`. |
| `incoming` | | Always `true`. |
| `sessionId` | `id` | Judge.me's internal review id, prefixed `judge-me-`. |
| `conversationId` | `id` | |
| `dimensionlabs_timestamp` | `created_at` | Convert with `toMillis`. |
| `platformJson` | `rating`, `product_external_id`, `product_handle`, `product_title`, `source`, `verified`, `curated`, `hidden`, `reviewer.external_id` | `product_external_id` is the product id in your e-commerce platform (for example Shopify), and `reviewer.external_id` the customer id there: the fields to join to your own data. |

**Personal data.** Judge.me returns the reviewer's email, name and phone, and the review's `ip_address`. Don't send them: the code uses Judge.me's ids instead. Reviews can still name people in their text: see [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// judge-me.mjs. Environment: JUDGEME_PRIVATE_TOKEN, JUDGEME_SHOP_DOMAIN, DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// Retries Judge.me's rate limits (429) and server errors (5xx), honoring Retry-After when it is sent.
async function get(url) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, { headers: { 'X-Api-Token': process.env.JUDGEME_PRIVATE_TOKEN, Accept: 'application/json' } });
    if (res.ok) return res.json();
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Judge.me returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Sends every review created or updated at or after `since` (Unix ms). Returns the new `since`.
export async function syncJudgeMe({ since, historical = false }) {
  const started = Date.now();
  for (let page = 1; ; page++) {
    const q = new URLSearchParams({ shop_domain: process.env.JUDGEME_SHOP_DOMAIN, per_page: '100', page: String(page) });
    const { reviews = [] } = await get(`https://api.judge.me/api/v1/reviews?${q}`);
    for (const r of reviews) {
      if (toMillis(r.updated_at ?? r.created_at) < since) continue; // unchanged since the last run
      const text = [r.title, r.body].filter((x) => x?.trim()).join('\n');
      if (!text) continue;
      const id = `judge-me-${r.id}`;
      const created = toMillis(r.created_at);
      await sendToDimensionLabs({ userId: r.reviewer?.id ? `judge-me-reviewer-${r.reviewer.id}` : id, sessionId: id, conversationId: id,
        text, incoming: true, dimensionlabs_timestamp: created,
        platformJson: { rating: r.rating, productExternalId: r.product_external_id, productHandle: r.product_handle,
          productTitle: r.product_title, source: r.source, verified: r.verified, curated: r.curated, hidden: r.hidden,
          reviewerExternalId: r.reviewer?.external_id } },
        { historical: historical || created < Date.now() - HOUR });
    }
    if (reviews.length < 100) break;
  }
  return started;
}
```

A review that is edited, curated or moved to another product comes back with a newer `updated_at` and is sent again under the same `sessionId`. Anything unchanged is a duplicate, and is removed automatically.

## 5. Run it

Save the code as `judge-me.mjs` in a project set up as in [Set up your project](/api/tracker#set-up-your-project), and run it with the [scheduled sync](/api/scheduled-sync) runner: `import { syncJudgeMe as sync } from './judge-me.mjs';`.

- **History first:** `node run.mjs 2020-01-01` sends every review since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then on a schedule:** the job sends new and changed reviews to `/track`, and they are enriched automatically every day. Anything more than an hour old when the job sees it goes to `/trackhistorical`, and needs a backfill. Every run reads all your reviews, because the API has no date filter: every 15 minutes is fine for a small store, but for thousands of reviews run it hourly or daily.
- **Re-runs are safe:** duplicates are removed automatically.

If you have reviews in a file instead, [upload it](/data/upload-a-file) and check it first with the [file checker](/data/check-a-file), then run a backfill.

## Judge.me reference

- [Using Judge.me API](https://judge.me/help/en/articles/8409180-using-judge-me-api)
- [Judge.me API documentation](https://judge.me/api/docs)
- [Build integrations with Judge.me](https://help.judge.me/en/articles/8278390-integrating-with-judge-me)

Checked against Judge.me's documentation on 2026-10-26.
