Skip to content

02 Get your data in /Integrations

Birdeye

Send Birdeye reviews from every review site and location to Dimension Labs with the universal API.

  1. B BirdeyeTickets, 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 Birdeye collects for your business locations, from Google, Facebook and the other review sites Birdeye aggregates, plus reviews left directly on Birdeye: each review as a customer message, with its rating and source, and your business response when there is one. One conversation per review. You run it on your side: a small script pulls from the Birdeye API and posts each message to the universal API.

  • A Birdeye account and its API key. Birdeye says the key can be fetched from the Birdeye dashboard and is confidential: call the API from your server only, never from a browser.
  • The Birdeye business ID of each location you want to bring in.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

Birdeye’s steps are in Authentication and rate limiting.

  1. Get your API key from the Birdeye dashboard. Store it in a secrets manager.
  2. Send it on every request in the x-api-key header. Birdeye moved the key from a query parameter to this header in February 2026.
  3. Find the business ID of each location. For an enterprise account, Birdeye’s Child Businesses API lists the businesses under the account.
Terminal window
curl -X POST -H "x-api-key: $BIRDEYE_API_KEY" -H "Content-Type: application/json" \
-d '{"fromTimestamp": 1759276800000, "statuses": ["all"]}' \
"https://api.birdeye.com/resources/v1/review/businessId/BUSINESS_ID?sindex=0&count=100"
  • “Get Reviews” is a POST with the filters in a JSON body. The response is an array of reviews.
  • Featured and non-featured: with no statuses, Birdeye returns featured reviews only. "statuses": ["all"] returns both.
  • Incremental: fromTimestamp and toTimestamp filter by review date, in Unix milliseconds. Birdeye returns the review date itself as a day (Jan 20, 2014), with no time.
  • Pagination: sindex (start index) and count. Birdeye doesn’t support a window past 100,000 records (sindex plus count must be 100,000 or less), so the code asks for 30 days at a time.
  • The business response is in the same review, under response and responseDate, so there is no second call.
  • Rate limits (Birdeye’s): each API key has a limit, and Birdeye asks you to contact its support team for the current one. Over the limit, it returns 429.

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

Dimension LabsBirdeyeNotes
texttitle and comments (incoming), response (outgoing)Join the title and body with a line break when there is a title. Reviews with no text are skipped.
userIdcustomerIdBirdeye’s id for the contact the review is attributed to, when there is one. Otherwise the sessionId.
incomingtrue for the review, false for the business response.
sessionIdreviewIdPrefixed birdeye-.
conversationIdreviewId
dimensionlabs_timestampreviewDate (review), responseDate (response)A day with no time, read as midnight UTC. A response on the same day as the review is moved a second later.
platformJsonbusiness ID, sourceType, rating, recommended, reviewURL, extReferenceId, reviewOperationalStatussourceType says which site the review came from. rating is 0 for direct feedback and Facebook recommendations. extReferenceId is your own reference for the location.

Personal data. Birdeye returns the reviewer’s name, and can return their email, contact details and custom fields (needCustomerInfo, fetchExtraParams) and the employees who helped (fetchAssitedByDetails). Don’t send any of them: the code leaves those options off and uses Birdeye’s ids instead. Reviews can still name people in their text: see PII redaction.

// birdeye.mjs. Environment: BIRDEYE_API_KEY, BIRDEYE_BUSINESS_IDS (comma-separated), DL_API_KEY.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
const WINDOW = 30 * DAY; // keeps each request well under Birdeye's 100,000-record window
const COUNT = 100;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Birdeye gives dates as a day ("Jan 20, 2014") or as Unix milliseconds. A day is read as midnight UTC.
function day(d) {
if (d == null || d === '') return null;
if (typeof d === 'number' || /^\d+$/.test(String(d))) return toMillis(Number(d));
const t = Date.parse(`${d} 00:00:00 UTC`);
return Number.isNaN(t) ? toMillis(d) : t;
}
// Retries Birdeye's rate limits (429) and server errors (5xx), honoring Retry-After when it is sent.
async function getReviews(businessId, sindex, body) {
const url = `https://api.birdeye.com/resources/v1/review/businessId/${businessId}?sindex=${sindex}&count=${COUNT}`;
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { method: 'POST', body: JSON.stringify(body),
headers: { 'x-api-key': process.env.BIRDEYE_API_KEY, Accept: 'application/json', 'Content-Type': 'application/json' } });
// Review ids can be longer than JavaScript numbers hold exactly, so read them as text.
if (res.ok) return JSON.parse((await res.text()).replace(/"reviewId"\s*:\s*(\d+)/g, '"reviewId":"$1"'));
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Birdeye returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
}
async function send(r, businessId, historical) {
const created = day(r.reviewDate);
if (created == null) return;
const id = `birdeye-${r.reviewId}`;
const base = { userId: r.customerId ? `birdeye-customer-${r.customerId}` : id, sessionId: id, conversationId: id,
platformJson: { businessId, source: r.sourceType, rating: r.rating, recommended: r.recommended,
reviewUrl: r.reviewURL ?? r.reviewUrl, extReferenceId: r.extReferenceId, status: r.reviewOperationalStatus } };
const text = [r.title, r.comments].filter((x) => x?.trim()).join('\n');
if (text) {
await sendToDimensionLabs({ ...base, text, incoming: true, dimensionlabs_timestamp: created },
{ historical: historical || created < Date.now() - HOUR });
}
if (r.response?.trim()) {
const replied = Math.max(day(r.responseDate) ?? created, created + 1000);
await sendToDimensionLabs({ ...base, text: r.response.trim(), incoming: false, dimensionlabs_timestamp: replied },
{ historical: historical || replied < Date.now() - HOUR });
}
}
// Sends every review dated on or after the day before `since` (Unix ms), for each location. Returns the new `since`.
export async function syncBirdeye({ since, historical = false }) {
const started = Date.now();
const ids = process.env.BIRDEYE_BUSINESS_IDS.split(',').map((x) => x.trim()).filter(Boolean);
for (const businessId of ids) {
// Review dates are days, so start a day early; anything sent twice is removed as a duplicate.
for (let from = since - DAY; from < started + DAY; from += WINDOW) {
const body = { fromTimestamp: from, toTimestamp: from + WINDOW, statuses: ['all'] };
for (let sindex = 0; sindex + COUNT <= 100000; sindex += COUNT) {
const reviews = await getReviews(businessId, sindex, body);
for (const r of reviews) await send(r, businessId, historical);
if (reviews.length < COUNT) break;
}
}
}
return started;
}

Each run asks for every review dated from the day before the cursor, so a day of reviews is sent again on each run: those repeats are duplicates, removed automatically. This picks reviews up by review date, so a business response written after the review was pulled is not sent. To catch those, run a history load over the last few weeks now and then (node run.mjs with a recent date): it sends the new responses, and everything else in it is a duplicate. Then run a backfill.

Save the code as birdeye.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncBirdeye as sync } from './birdeye.mjs';. One Dimension Labs source can hold all your locations: the business ID goes in platformJson.businessId.

  • History first: node run.mjs 2020-01-01 sends every review since that date to /trackhistorical, 30 days at a time. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends new reviews. Because Birdeye dates a review by its day, most reviews are already more than an hour “old” when the job sees them, so they go to /trackhistorical. Run a backfill over recent dates each week so they are enriched.
  • Re-runs are safe: duplicates are removed automatically.
  • Rate limits: Birdeye doesn’t publish its limit. The code waits and retries on 429. With many locations, ask Birdeye support for your limit before a big history load.

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

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