02 Get your data in /Integrations
Birdeye
Send Birdeye reviews from every review site and location to Dimension Labs with the universal API.
- B BirdeyeTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- 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.
What you need
Section titled “What you need”- 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.
1. Get API access in Birdeye
Section titled “1. Get API access in Birdeye”Birdeye’s steps are in Authentication and rate limiting.
- Get your API key from the Birdeye dashboard. Store it in a secrets manager.
- Send it on every request in the
x-api-keyheader. Birdeye moved the key from a query parameter to this header in February 2026. - Find the business ID of each location. For an enterprise account, Birdeye’s Child Businesses API lists the businesses under the account.
2. Pull new reviews
Section titled “2. Pull new reviews”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
POSTwith 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:
fromTimestampandtoTimestampfilter 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) andcount. Birdeye doesn’t support a window past 100,000 records (sindexpluscountmust be 100,000 or less), so the code asks for 30 days at a time. - The business response is in the same review, under
responseandresponseDate, 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.
3. Map the fields
Section titled “3. Map the fields”Each review is one incoming message under its own sessionId. A business response is an outgoing message in the same session.
| Dimension Labs | Birdeye | Notes |
|---|---|---|
text | title 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. |
userId | customerId | Birdeye’s id for the contact the review is attributed to, when there is one. Otherwise the sessionId. |
incoming | true for the review, false for the business response. | |
sessionId | reviewId | Prefixed birdeye-. |
conversationId | reviewId | |
dimensionlabs_timestamp | reviewDate (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. |
platformJson | business ID, sourceType, rating, recommended, reviewURL, extReferenceId, reviewOperationalStatus | sourceType 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.
4. Send it
Section titled “4. Send it”// 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 windowconst 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.
5. Run it
Section titled “5. Run it”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-01sends 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.
Birdeye reference
Section titled “Birdeye reference”Checked against Birdeye’s documentation on 2026-10-26.