# Brandwatch Consumer Research

> Send the mentions matched by a Brandwatch Consumer Research Query to Dimension Labs with the universal API.

This brings in the mentions your Brandwatch Consumer Research Query already collects: forum and blog posts, reviews, news and social posts about your brand, each one as a customer message with Brandwatch's sentiment and categories alongside. You run it on your side: a small script pulls from the Consumer Research API and posts each mention to the [universal API](/api/tracker).

## What you need

- A Brandwatch Consumer Research account and a Query that collects the mentions you want.
- A Consumer Research user with the **Regular** or **Admin** role. Brandwatch says only those users can use the API.
- A Dimension Labs API key for a new **Universal API** source. See [Your API key](/api/tracker#your-api-key).

**Sources Brandwatch can't pass in full.** Brandwatch's [Data Restrictions](https://developers.brandwatch.com/docs/data-restrictions) page says some data packs limit what the API returns:

- **X (Twitter):** the full text and most metadata are stripped. Brandwatch suggests taking the post id from `guid` and fetching the text from the X API yourself; see the [X guide](/integrations/x), and its note on X's terms.
- **Online News:** only a 256-character snippet is available.
- **LinkedIn:** no snippet and no full text.

Brandwatch's changelog also announced Reddit restrictions from January 2026. Ask your account manager which restrictions apply to your data packs. Mentions with no text are skipped by the code below.

## 1. Get API access in Brandwatch

Brandwatch's steps are in [Authentication](https://developers.brandwatch.com/docs/authenticate).

1. Get an access token with the API user's username and password, once:

```bash
curl -X POST --data-urlencode "password=$BRANDWATCH_PASSWORD" \
  "https://api.brandwatch.com/oauth/token?username=api.user@yourcompany.com&grant_type=api-password&client_id=brandwatch-api-client"
```

2. The answer's `access_token` is valid for one year by default (`expires_in` is in seconds). Keep it in a secrets manager and renew it before it expires. If your user can switch between organizations, add `platform_client_id` to the request; Brandwatch support gives you the ids.
3. Send it as `Authorization: Bearer ACCESS_TOKEN`.
4. Find your Project ID and Query ID with Brandwatch's [Retrieving Projects](https://developers.brandwatch.com/docs/retrieving-projects) and [Retrieving Queries](https://developers.brandwatch.com/docs/retrieving-queries) calls.

## 2. Pull new mentions

```bash
curl -G "https://api.brandwatch.com/projects/PROJECT_ID/data/mentions/fulltext" \
  -H "Authorization: Bearer $BRANDWATCH_TOKEN" \
  --data-urlencode "queryId=QUERY_ID" \
  --data-urlencode "startDate=2026-10-01T00:00:00.000+0000" --data-urlencode "endDate=2026-10-26T00:00:00.000+0000" \
  --data-urlencode "pageSize=1" --data-urlencode "page=0"
```

- `/data/mentions` returns a `snippet` of each mention, the part that best matches the Query. `/data/mentions/fulltext` returns the full text, which is what you want to enrich.
- **Find the text field once.** Brandwatch's docs don't name the field that holds the full text in a `fulltext` answer. Run the call above with `pageSize=1`, look at the mention, and set `BRANDWATCH_TEXT_FIELD` to the name of the field that holds the whole post.
- **Dates:** `startDate` and `endDate` are required and filter on when the mention was posted. Format: `2026-10-01T00:00:00.000+0000`; encode the `+` as `%2B` (`--data-urlencode` and the code do this).
- **Incremental:** each mention has `added`, the time it was added to the Query, which can be well after it was posted. The code reads the last 24 hours of posting dates (`BRANDWATCH_LOOKBACK_HOURS`) and keeps mentions added since the last run, with the 5-minute buffer Brandwatch's [polling tutorial](https://developers.brandwatch.com/docs/tutorial-polling-for-mentions) uses. A Query backfill resets `added`, so backfilled mentions are sent again; the duplicates are removed.
- **Pagination:** `pageSize` goes up to 5,000, and `page` counts from 0. Page-based reads stop at 10,000 mentions, so the code splits a busier date range in two.
- **Rate limits (Brandwatch's):** 30 requests every 10 minutes per client by default, with a `429` past that. The `x-rate-limit` and `x-rate-limit-used` headers show your usage. Brandwatch asks for one request at a time rather than in parallel, and your account manager can discuss a higher limit.

## 3. Map the fields

Each mention is one incoming message under its own `sessionId`. Brandwatch gives no reliable flag for posts by your own accounts, so exclude your own accounts in the Query itself.

| Dimension Labs | Brandwatch | Notes |
|---|---|---|
| `text` | the full-text field (see step 2) | Skip mentions with no text. |
| `userId` | `resourceId` | Author ids differ by network, so use the mention's id. Don't send `author` or `fullname`. |
| `incoming` | | Always `true`. |
| `sessionId` | `resourceId` | Brandwatch's unique id for the mention. Prefix it: `brandwatch-c54d12f5107496bac30b5ddc3478bbab`. |
| `conversationId` | `resourceId` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | `date` | When the mention was posted, like `2020-06-29T22:45:43.000+0000`. Convert with `toMillis`. |
| `platformJson` | `queryId`, `contentSource`, `domain`, `sentiment`, `language`, `countryCode`, `threadId`, `threadEntryType`, `guid`, `tags`, `categories` | `contentSource` tells the networks apart. `threadId` (X and Facebook only) groups mentions from one thread. |

**Personal data.** Don't send `author`, `fullname`, `avatarUrl` or `url`, which can hold names and handles. Mention text can hold names too: redact it before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// brandwatch.mjs. Environment: BRANDWATCH_TOKEN, BRANDWATCH_PROJECT_ID, BRANDWATCH_QUERY_ID,
// BRANDWATCH_TEXT_FIELD (the field that holds the full text in a fulltext response; see step 2), DL_API_KEY.
// Optional: BRANDWATCH_LOOKBACK_HOURS (default 24).


const HOUR = 60 * 60 * 1000;
const BUFFER = 5 * 60 * 1000; // Brandwatch's polling tutorial uses a 5-minute buffer on `added`
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const API = `https://api.brandwatch.com/projects/${process.env.BRANDWATCH_PROJECT_ID}/data/mentions/fulltext`;
const bwTime = (ms) => new Date(ms).toISOString().replace('Z', '+0000'); // 2026-10-01T00:00:00.000+0000

// Retries Brandwatch's rate limit (429: 30 calls per 10 minutes by default) and server errors (5xx).
async function get(params) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(`${API}?${new URLSearchParams(params)}`, { headers: { Authorization: `Bearer ${process.env.BRANDWATCH_TOKEN}` } });
    if (res.ok) return res.json();
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Brandwatch returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 60 * 2 ** attempt) * 1000);
  }
}

// Every mention posted between `start` and `end`. Page-based reads stop at 10,000 mentions, so a busier window is split in two.
async function* mentions(start, end) {
  const base = { queryId: process.env.BRANDWATCH_QUERY_ID, startDate: bwTime(start), endDate: bwTime(end),
    pageSize: '5000', orderBy: 'date', orderDirection: 'asc' };
  const first = await get({ ...base, page: '0' });
  if (first.resultsTotal > 10000 && end - start > HOUR) {
    const mid = start + Math.floor((end - start) / 2);
    yield* mentions(start, mid);
    yield* mentions(mid, end);
    return;
  }
  yield first.results ?? [];
  for (let page = 1; page * 5000 < Math.min(first.resultsTotal ?? 0, 10000); page++) {
    yield (await get({ ...base, page: String(page) })).results ?? [];
  }
}

// Sends every mention added to the Query at or after `since` (Unix ms). Returns the new `since`.
export async function syncBrandwatch({ since, historical = false }) {
  const field = process.env.BRANDWATCH_TEXT_FIELD;
  if (!field) throw new Error('Set BRANDWATCH_TEXT_FIELD to the field that holds the full text (see step 2 of the guide).');
  let newest = since;
  const start = historical ? since : since - Number(process.env.BRANDWATCH_LOOKBACK_HOURS || 24) * HOUR; // mentions are often added late
  for await (const batch of mentions(start, Date.now())) {
    for (const m of batch) {
      const added = toMillis(m.added);
      if (!historical && added < since - BUFFER) continue; // seen on an earlier run
      newest = Math.max(newest, added);
      const text = String(m[field] ?? '').trim();
      if (!text) continue; // X posts and LinkedIn data come without text
      const ts = toMillis(m.date);
      const sessionId = `brandwatch-${m.resourceId}`;
      await sendToDimensionLabs({
        text,
        userId: sessionId,
        incoming: true,
        sessionId,
        conversationId: sessionId,
        dimensionlabs_timestamp: ts,
        platformJson: { queryId: m.queryId, contentSource: m.contentSource, domain: m.domain, sentiment: m.sentiment,
          language: m.language, countryCode: m.countryCode, threadId: m.threadId, threadEntryType: m.threadEntryType,
          guid: m.guid, tags: m.tags, categories: m.categories },
      }, { historical: historical || ts < Date.now() - HOUR });
    }
  }
  return newest;
}
```

## 5. Run it

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

- **History first:** `node run.mjs 2026-01-01` sends every mention posted since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every 15 minutes:** the scheduled job sends new mentions 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.

The default limit of 30 requests every 10 minutes is the constraint. A 15-minute run makes one or two calls; a history load of a busy Query makes many, and the code waits whenever Brandwatch returns a `429`. If you have mentions 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.

## Brandwatch reference

- [Authentication](https://developers.brandwatch.com/docs/authenticate)
- [Rate limiting](https://developers.brandwatch.com/docs/rate-limiting)
- [Best practices & our terms](https://developers.brandwatch.com/docs/best-practices)
- [Retrieving Mentions](https://developers.brandwatch.com/docs/retrieving-mentions)
- [Mention Field Definitions](https://developers.brandwatch.com/docs/mention-metadata-field-definitions)
- [Data Restrictions](https://developers.brandwatch.com/docs/data-restrictions)
- [Paging through historical Mentions](https://developers.brandwatch.com/docs/tutorial-paging-through-historical-mentions)
- [Polling for Mentions](https://developers.brandwatch.com/docs/tutorial-polling-for-mentions)
- [Reddit Data Restrictions (changelog)](https://developers.brandwatch.com/changelog/reddit-data-restrictions)

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