Skip to content

02 Get your data in /Integrations

Yotpo

Send Yotpo product and site reviews to Dimension Labs with the universal API.

  1. Y YotpoTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. Dimension LabsRead in full · enriched daily

This brings in the reviews in your Yotpo store, product reviews and site (store) reviews: each review as a customer message, with its score, product and Yotpo’s sentiment score. One conversation per review. You run it on your side: a small script pulls from the Yotpo API and posts each message to the universal API.

  • Your Yotpo store ID (formerly app key) and API secret. Yotpo says only the account administrator can see the API secret; teammates with Staff permissions can’t.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

Yotpo has no sandbox: every call reaches a real store. The calls in this guide only read data.

Yotpo’s steps are in Store ID and API Secret and Authenticate and make your first call.

  1. Have the account administrator copy the store ID and API secret. Yotpo’s help article shows where they are. Keep the secret in a secrets manager.
  2. Get an access token from your server:
Terminal window
curl -X POST https://api.yotpo.com/oauth/token -H "Content-Type: application/json" \
-d '{"client_id": "YOUR_STORE_ID", "client_secret": "YOUR_API_SECRET", "grant_type": "client_credentials"}'
  1. Send the token in the X-Yotpo-Token header. Yotpo asks you to store the token and reuse it, not request a new one for every call. If a call returns 401, get a new token and retry once. The code below does this.
Terminal window
curl -H "X-Yotpo-Token: $YOTPO_TOKEN" -H "Accept: application/json" \
"https://api.yotpo.com/v1/apps/YOUR_STORE_ID/reviews?since_updated_at=2026-10-01&count=100&page=1"
  • “Retrieve all reviews” returns product and site reviews together. Site reviews have sku set to yotpo_site_reviews.
  • Incremental: since_updated_at returns reviews updated on or after a date, so an edited review comes back too. since_date filters by creation date instead.
  • Pagination: page and count. Yotpo recommends up to 100 reviews per request; the default is 10.
  • Delay: Yotpo says data on this endpoint comes with a delay of about an hour.
  • Deleted reviews are left out unless you pass deleted=true.
  • Review text is HTML-encoded (for example ' for an apostrophe). The code decodes it.
  • This endpoint doesn’t return store replies to reviews, so only the review is sent.
  • Rate limits (Yotpo’s): 5,000 requests a minute per store ID for this endpoint. Over the limit, Yotpo returns 429, with a ratelimit-reset header in seconds.

Each review is one incoming message under its own sessionId.

Dimension LabsYotpoNotes
texttitle and contentJoin the title and body with a line break. HTML entities are decoded.
userIdYotpo returns no internal reviewer id, so use the sessionId. Don’t send name or email.
incomingAlways true.
sessionIdidPrefixed yotpo-.
conversationIdid
dimensionlabs_timestampcreated_atISO 8601. Convert with toMillis.
platformJsonscore, sku, sentiment, votes_up, votes_down, reviewer_type, is_incentivized, incentive_type, user_referencesku is your product ID, the field to join to your product data. user_reference is the external user reference sent when the review was created, if your store sends one.

Personal data. Yotpo returns the reviewer’s name and email address on every review. Don’t send them: the code uses the review id instead. Reviews can still name people in their text: see PII redaction.

// yotpo.mjs. Environment: YOTPO_STORE_ID, YOTPO_API_SECRET, DL_API_KEY.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
const LAG = 3 * HOUR; // Yotpo's data arrives about an hour late; look back further than that
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const API = 'https://api.yotpo.com';
let token = null; // reused while the process lives, as Yotpo asks
async function accessToken() {
if (token) return token;
const res = await fetch(`${API}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: process.env.YOTPO_STORE_ID, client_secret: process.env.YOTPO_API_SECRET, grant_type: 'client_credentials' }) });
if (!res.ok) throw new Error(`Yotpo token request returned ${res.status}: ${await res.text()}`);
token = (await res.json()).access_token;
return token;
}
// Retries Yotpo's rate limits (429) and server errors (5xx), honoring Retry-After or ratelimit-reset.
async function get(url) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { headers: { 'X-Yotpo-Token': await accessToken(), Accept: 'application/json' } });
if (res.ok) return res.json();
if (res.status === 401 && attempt === 0) { token = null; continue; }
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Yotpo returned ${res.status}: ${await res.text()}`);
const wait = Number(res.headers.get('retry-after')) || Number(res.headers.get('ratelimit-reset')) || 2 ** attempt;
await sleep(wait * 1000);
}
}
const NAMED = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
const decode = (s) => String(s ?? '').replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, e) =>
/^#x/i.test(e) ? String.fromCodePoint(parseInt(e.slice(2), 16))
: e[0] === '#' ? String.fromCodePoint(parseInt(e.slice(1), 10)) : NAMED[e.toLowerCase()] ?? m);
// Sends every review created or updated at or after `since` (Unix ms). Returns the new `since`.
export async function syncYotpo({ since, historical = false }) {
const started = Date.now();
const from = new Date(since - DAY).toISOString().slice(0, 10); // since_updated_at takes a date
for (let page = 1; ; page++) {
const q = new URLSearchParams({ since_updated_at: from, count: '100', page: String(page) });
const { reviews = [] } = await get(`${API}/v1/apps/${process.env.YOTPO_STORE_ID}/reviews?${q}`);
for (const r of reviews) {
if (toMillis(r.updated_at) < since - LAG) continue; // already sent by an earlier run
const text = [decode(r.title).trim(), decode(r.content).trim()].filter(Boolean).join('\n');
if (!text) continue;
const id = `yotpo-${r.id}`;
const created = toMillis(r.created_at);
await sendToDimensionLabs({ userId: id, sessionId: id, conversationId: id, text, incoming: true, dimensionlabs_timestamp: created,
platformJson: { score: r.score, sku: r.sku, siteReview: r.sku === 'yotpo_site_reviews', sentiment: r.sentiment,
votesUp: r.votes_up, votesDown: r.votes_down, reviewerType: r.reviewer_type, incentivized: r.is_incentivized,
incentiveType: r.incentive_type, userReference: r.user_reference } },
{ historical: historical || created < Date.now() - HOUR });
}
if (reviews.length < 100) break;
}
return started;
}

A review that is edited comes back with a newer updated_at and is sent again under the same sessionId. Anything unchanged is a duplicate, and is removed automatically. The code looks back three hours past the cursor to allow for Yotpo’s delay.

Save the code as yotpo.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncYotpo as sync } from './yotpo.mjs';.

  • History first: node run.mjs 2020-01-01 sends every review updated since that date to /trackhistorical. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends new reviews to /track, and they are enriched automatically every day. Because of Yotpo’s delay, some new reviews are already more than an hour old when the job sees them: those go to /trackhistorical, and need a backfill. Run one over recent dates each week.
  • Re-runs are safe: duplicates are removed automatically.
  • Rate limits: 5,000 requests a minute is far more than this job uses. The code waits and retries on 429.

If you have reviews in a file instead, upload it and check it first with the file checker, then run a backfill.

Checked against Yotpo’s documentation on 2026-10-26.