# Bazaarvoice

> Send Bazaarvoice product reviews to Dimension Labs with the universal API.

This brings in the product reviews in your Bazaarvoice client instance: each review as a customer message, with its rating and product, and your brand responses when there are any. One conversation per review. You run it on your side: a small script pulls from the Bazaarvoice Conversations API and posts each message to the [universal API](/api/tracker).

## What you need

- **Bazaarvoice's written approval to store review content.** The Conversations API is built to display reviews on your own site and apps. Bazaarvoice's [API Terms of Use](https://www.bazaarvoice.com/legal/terms-of-use/conversations-api/) say you may not store data retrieved through it, and point to its Displayable Content Export, sold separately, for backend data management. Its rate-limit guide says using the Conversations API to build your own data store is limited to clients with prior approval. Sending reviews to Dimension Labs stores them, so get approval from your Bazaarvoice representative before you run this guide, or use the export described at the end of "Run it".
- API access enabled for your client instance (if it isn't, contact your Bazaarvoice representative), and a Portal account with the **Technical Contributor** or **Technical Administrator** role and the **Ratings and Reviews** solution.
- A Conversations API key (passkey), approved by a Technical Administrator.
- A key set up to exclude syndicated content, so it can list all your reviews without a product filter (see step 2). Bazaarvoice support can configure this.
- 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 Bazaarvoice

Bazaarvoice's steps are in [API key management](https://developers.bazaarvoice.com/v1.0-ConversationsAPI/docs/api-key-management).

1. Sign in to [Bazaarvoice Portal](https://portal.bazaarvoice.com) and pick your client instance in the **Instance Selector** in the upper-right corner.
2. Select **Settings**, then **API** under **Developer**.
3. Select **Request API key**, choose **Conversations API**, select **Next**, and fill out the form. You receive the key details by email.
4. Your **Technical Administrator** approves it: in the API Keys section, filter **Key details > Status** to **Pending**, open the key, select **Enable Key**, then **Save**.
5. The passkey goes in the `passkey` query parameter. Production is `https://api.bazaarvoice.com`; staging (`https://stg.api.bazaarvoice.com`) has data that doesn't come from real users.

## 2. Pull new reviews

```bash
curl -H "bv-version: 5.4.1" \
  "https://api.bazaarvoice.com/data/reviews.json?apiversion=5.4&passkey=$BV_PASSKEY&Filter=LastModificationTime:gte:1759276800&Sort=LastModificationTime:asc&Limit=100&Offset=0"
```

- **All reviews:** a request with no `ProductId`, `AuthorId`, `CategoryAncestorId`, `SubmissionId` or review `Id` filter only works with a key configured to exclude syndicated content. Bazaarvoice recommends its Displayable Content Export for pulling everything, native and syndicated (see "Run it").
- **Incremental:** `Filter=LastModificationTime:gte:SECONDS` returns reviews changed since a time. Bazaarvoice's time filters take seconds since 1970, UTC. Sort with `Sort=LastModificationTime:asc` (a sort order is required).
- **Pagination:** `Limit` (up to 100, default 10) and `Offset` (up to 300,000).
- Brand responses are in the same review, under `ClientResponses`, so there is no second call.
- **Errors:** by default Bazaarvoice returns `200 OK` with the error in the body. The `bv-version: 5.4.1` header makes it return real `4xx` and `5xx` codes. The code sends it, and also checks `HasErrors`.
- **Rate limits (Bazaarvoice's):** per key, per minute. Your key's limit is in the Portal and in the `X-Bazaarvoice-QPM-Allotted` header; `X-Bazaarvoice-Quota-Allotted` is the hourly quota. Bazaarvoice says increases must be approved and may cost extra.
- Bazaarvoice's Terms of Use prohibit XML output: use `reviews.json`.

## 3. Map the fields

Each review is one incoming message under its own `sessionId`. Each brand response is an outgoing message in the same session.

| Dimension Labs | Bazaarvoice | Notes |
|---|---|---|
| `text` | `Title` and `ReviewText` (incoming), `ClientResponses[].Response` (outgoing) | Join the title and body with a line break. Ratings-only reviews have no text and are skipped. |
| `userId` | `AuthorId` | Prefixed `bazaarvoice-`. Don't send `UserNickname` or `UserLocation`. |
| `incoming` | | `true` for the review, `false` for a brand response. |
| `sessionId` | `Id` | Prefixed `bazaarvoice-`. |
| `conversationId` | `Id` | |
| `dimensionlabs_timestamp` | `SubmissionTime` (review), `ClientResponses[].Date` (response) | Convert with `toMillis`. A response within a second of the review is moved a second later. |
| `platformJson` | `Rating`, `ProductId`, `IsRecommended`, `ContentLocale`, `IsSyndicated`, `IsFeatured`, `ModerationStatus`, `CampaignId`, `Pros`, `Cons` | `ProductId` is the field to join to your product data. |

**Personal data.** Reviews carry the author's nickname and location, and can carry context data such as age. Don't send them: the code uses `AuthorId` instead. Reviews can still name people in their text: see [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// bazaarvoice.mjs. Environment: BV_PASSKEY, DL_API_KEY. Optional: BV_HOST (defaults to production).


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const HOST = process.env.BV_HOST ?? 'https://api.bazaarvoice.com';
const MAX_OFFSET = 300000;
const at = (d, fallback) => { try { return d ? toMillis(d) : fallback; } catch { return fallback; } };

// Retries Bazaarvoice'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: { 'bv-version': '5.4.1', Accept: 'application/json' } });
    if (res.ok) {
      const body = await res.json();
      if (body.HasErrors) throw new Error(`Bazaarvoice returned errors: ${JSON.stringify(body.Errors)}`);
      return body;
    }
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Bazaarvoice returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

async function send(r, historical) {
  const id = `bazaarvoice-${r.Id}`;
  const created = toMillis(r.SubmissionTime);
  const base = { userId: r.AuthorId ? `bazaarvoice-${r.AuthorId}` : id, sessionId: id, conversationId: id,
    platformJson: { rating: r.Rating, productId: r.ProductId, recommended: r.IsRecommended, locale: r.ContentLocale,
      syndicated: r.IsSyndicated, featured: r.IsFeatured, moderationStatus: r.ModerationStatus, campaignId: r.CampaignId,
      pros: r.Pros, cons: r.Cons } };
  const text = [r.Title, r.ReviewText].filter((x) => x?.trim()).join('\n');
  if (text) {
    await sendToDimensionLabs({ ...base, text, incoming: true, dimensionlabs_timestamp: created },
      { historical: historical || created < Date.now() - HOUR });
  }
  let last = created;
  for (const c of r.ClientResponses ?? []) {
    if (!c.Response?.trim()) continue;
    last = Math.max(at(c.Date, last), last + 1000);
    await sendToDimensionLabs({ ...base, text: c.Response.trim(), incoming: false, dimensionlabs_timestamp: last,
      platformJson: { ...base.platformJson, responseType: c.ResponseType, department: c.Department } },
      { historical: historical || last < Date.now() - HOUR });
  }
}

// Sends every review changed at or after `since` (Unix ms). Returns the new `since`.
export async function syncBazaarvoice({ since, historical = false }) {
  let newest = since, from = since;
  for (;;) {
    let restart = false;
    for (let offset = 0; ; offset += 100) {
      if (offset + 100 > MAX_OFFSET) { restart = true; break; } // past Bazaarvoice's offset limit: start again from the newest seen
      const q = new URLSearchParams({ apiversion: '5.4', passkey: process.env.BV_PASSKEY,
        Filter: `LastModificationTime:gte:${Math.floor(from / 1000)}`, Sort: 'LastModificationTime:asc', Limit: '100', Offset: String(offset) });
      const { Results = [] } = await get(`${HOST}/data/reviews.json?${q}`);
      for (const r of Results) {
        newest = Math.max(newest, at(r.LastModificationTime, newest));
        await send(r, historical);
      }
      if (Results.length < 100) break;
    }
    if (!restart || newest === from) return newest;
    from = newest;
  }
}
```

A review that changes (an edit, a new brand response, a moderation decision) comes back with a newer `LastModificationTime` and is sent again under the same `sessionId`. Anything unchanged is a duplicate, and is removed automatically. Comments other shoppers leave on a review are not sent; Bazaarvoice returns them with `Include=Comments` if you want to add them.

## 5. Run it

Save the code as `bazaarvoice.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 { syncBazaarvoice as sync } from './bazaarvoice.mjs';`.

- **History first:** `node run.mjs 2020-01-01` sends every review changed since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every 15 minutes:** the scheduled job sends new reviews and responses 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.
- **Re-runs are safe:** duplicates are removed automatically.
- **Rate limits:** a big history load can use up your per-minute or hourly allowance. The code waits and retries on `429`; check the `X-Bazaarvoice-QPM-Allotted` header to see your key's limit.

**If Bazaarvoice doesn't approve the Conversations API for this,** use its [Displayable Content Export](https://developers.bazaarvoice.com/v1.0-DCE/docs/getting-started) (DCE), which Bazaarvoice recommends for downloading all native and syndicated content and offers, for an added fee, for backend data management. It gives a full data set every Monday and an incremental one on the other days, using a passkey and a shared secret key from Bazaarvoice. Data sets are available about 24 hours after the DCE key is first approved. Convert the files to a CSV with the [converter](/data/convert-a-file), check it with the [file checker](/data/check-a-file), [upload it](/data/upload-a-file), then run a backfill.

## Bazaarvoice reference

- [Review Display](https://developers.bazaarvoice.com/docs/review-display)
- [Retrieve Reviews](https://developers.bazaarvoice.com/v1.0-ConversationsAPI/reference/get_data-reviews-json)
- [Platform and API concepts](https://developers.bazaarvoice.com/v1.0-ConversationsAPI/docs/platform-api-concepts)
- [API key management](https://developers.bazaarvoice.com/v1.0-ConversationsAPI/docs/api-key-management)
- [API rate limit](https://developers.bazaarvoice.com/v1.0-ConversationsAPI/docs/api-rate-limit)
- [Displayable Content Export: Getting started](https://developers.bazaarvoice.com/v1.0-DCE/docs/getting-started)
- [API Terms of Use](https://www.bazaarvoice.com/legal/terms-of-use/conversations-api/)

Checked against Bazaarvoice's documentation on 2026-10-26.
