Purchased contact and company data use credits separately from Free Mode. Check the lookup cost before enriching a large list. For website research without a paid lookup, start with public company pages. Compare plans.
rtrvr Enrich appends external data to your extracted results and sheets by matching them against Bright Data's dataset marketplace: LinkedIn people and companies, Crunchbase, Instagram, Amazon, Zillow, Indeed, and Google Maps. It runs inside a code plan via the rtrvr.enrich(...) helper. Your first 1,000 enrich records and web searches each month are free (one shared allowance), and anything beyond that bills automatically in credits at cost, no approval prompts.
Available Datasets
Reference a dataset in a plan with the rtrvr.datasets constants (e.g. rtrvr.datasets.linkedinPeople). The links below open each dataset's page in the Bright Data marketplace.
| Dataset | rtrvr.datasets key | Modes |
|---|---|---|
| LinkedIn people profiles | linkedinPeople | Instant lookup + Live scrape |
| LinkedIn people + business contact info (emails) | linkedinPeopleContact | Instant lookup only (a live scrape on it is served by linkedinPeople: profile fields, no email/phone) |
| LinkedIn company information | linkedinCompanies | Instant lookup + Live scrape |
| Crunchbase companies | crunchbaseCompanies | Live scrape only |
| Instagram profiles | instagramProfiles | Live scrape only |
| Amazon products | amazonProducts | Live scrape only |
| Zillow properties | zillowProperties | Live scrape only |
| Indeed job listings | indeedJobs | Live scrape only |
| Google Maps businesses | googleMaps | Live scrape only; business details and top reviews |
| Google Maps reviews | googleMapsReviews | Asynchronous live review collection |
Fields
Each dataset exposes dozens of fields. The most commonly used ones are below: call field discovery (rtrvr.enrich({ dataset, fields: true }), free) to get the full, live list before you filter, since wrong field names fail the job.
| Dataset | Key fields |
|---|---|
linkedinPeople / linkedinPeopleContact | id (profile slug: the lookup key), url, name, position (role/title), current_company_name, city, about (bio), experience. The contact variant adds email and cellphone_number: partial coverage, most profiles return null for both. |
linkedinCompanies | url, name, website, industries, company_size, headquarters |
crunchbaseCompanies | Company profile fields (name, about, industries, founded date, size, region, website, socials): use field discovery for the full list |
instagramProfiles | Profile fields (profile name/id, followers, posts, website, bio, verification): use field discovery for the full list |
amazonProducts | asin, url, title, brand, initial_price, currency, availability, reviews_count, categories: 113 fields in total, use field discovery |
zillowProperties | zpid, url, address, price, bedrooms, bathrooms, homeStatus, yearBuilt: 140 fields in total, use field discovery |
indeedJobs | jobid, url, job_title, company_name, location, salary_formatted, job_type, date_posted_parsed, description_text |
googleMaps | place_id, url, name, address, category, reviews_count, open_hours, business_details |
Two Modes: Instant Lookup vs Live Scrape
Enrichment runs one of two ways. Instant lookup queries Bright Data's pre-collected index and is the default for people and company enrichment; live scrape fetches fresh data on demand for exact URLs or datasets the index doesn't serve.
| Instant Lookup | Live Scrape | |
|---|---|---|
| API | Bright Data Search API | Web Scraper API (/scrape; reviews use async /trigger) |
| Speed | Sub-second | Often 10–30s; review jobs can take several minutes |
| Cost | Free up to the monthly allowance, then 0.25 credits / returned record | Free up to the monthly allowance, then 0.15 credits / successful record |
| Datasets | The 3 LinkedIn datasets only | Any scraper-backed dataset (Amazon, Zillow, Indeed, Google Maps, Crunchbase, Instagram) |
| How to call | { dataset, filter, fields } | { dataset, scrapeUrls, fields } |
| Best for | Bulk lookups by field (e.g. profile id) | Exact URLs, non-LinkedIn data, or lookup misses |
| Limits | Pass all keys in one in filter: batching is server-side | Max 20 inputs per call: chunk larger lists with rtrvr.mapLimit |
Slow Scrapes: Pending Jobs
Bright Data serves a scrape inline only while it fits their one-minute window. A heavier job (20 Zillow listings, say) hands back a job handle instead, and the call returns { pending: true, snapshotId } having billed nothing. Do other work, then collect the records later in the same plan:
let { records, pending, snapshotId } = await rtrvr.enrich({
dataset: rtrvr.datasets.zillowProperties,
scrapeUrls: listingUrls, // max 20 per call
fields: ['zpid', 'address', 'price', 'bedrooms'],
});
// ...do other free work here while Bright Data finishes...
if (pending) {
({ records, pending } = await rtrvr.enrich({ collect: snapshotId, fields: ['zpid', 'address', 'price', 'bedrooms'] }));
}
// Never re-scrape a pending job — collecting it is how you avoid paying twice.Google Maps Reviews
Use googleMaps for business details and a small top_reviews sample. Use googleMapsReviews (gd_luzfs1dn2oa0teb81) for fresh review records. Both use Bright Data's Web Scraper API. An explicit request to use the Maps UI takes priority: the agent should use that rendered page and ask before switching to a dataset or API if blocked. A running job is not a confirmed failure based on age alone. The reviews route starts an asynchronous job and returns its snapshot without keeping the agent waiting; provider collection can take several minutes.
const job = await rtrvr.enrich({
dataset: 'googleMapsReviews',
scrapeUrls: [placeUrl], // a resolved https://www.google.com/maps/place/... URL
limitPerInput: 120,
sortBy: 'Newest',
// daysLimit: 90, // optional age filter, in days
});
// Save job.snapshotId. If pending, end this run and collect later.
// Reuse the SAME account and billing scope. Do not dispatch the scrape again.
const result = await rtrvr.enrich({ collect: savedSnapshotId });- 01
limitPerInputis required for an intentional count: default 20, maximum 500 per place and 1,000 across a job. Larger requests fail validation rather than silently reducing the count. Split businesses into smaller jobs. Numeric strings and camelCase/snake_case control aliases are normalized; conflicting or misplaced count controls require correction before a paid request. This is a maximum, not a promise that the provider can access that many reviews. - 02
sortByaccepts Newest (also latest), Most relevant, Highest rating, or Lowest rating. Per-rowsort_byanddays_limitare supported withscrape: [{url, sort_by, days_limit}]. OmitdaysLimitto avoid a requested age restriction. - 03
Default fields retain full
reviewtext,review_id,place_id, rating, date, owner response and the displayed review total.review: nullmeans no written text was returned. It may be rating-only, but is not proof that no written text exists. Deduplicate locally by place and review ID. A limit, no-growth scroll, or displayed review count alone does not prove end-of-feed completion. - 04
Billing counts successful review records, including rating-only records, at the configured scrape rate (currently 0.15 credits per record beyond the shared free allowance). Failed inputs and pending collections are unbilled. Collecting an already billed snapshot costs zero additional enrichment credits and returns the same snapshot; it does not fetch the next review page. Agent/model/browser costs are separate.
- 05
A terminal provider failure returns
success: false,snapshotId,failures, andcreditsUsed. Preserve this result. Ano_peerserror comes from the provider scraper, not an empty successful collection. Do not redispatch an unknown or pending job. Any retry after a confirmed failure creates a new collection; there is no automatic provider retry or switch to another source. - 06
Review results include
collectioncounts andcompleteness: unverified. Set the count, sort and date window from your actual task. For all-review requests, use the displayed total to plan a bounded collection and report any gap; request limits and the displayed total alone do not establish completion. - 07
Snapshots retain original dataset, projection and record limit. Collection checks ownership and billing scope. If billing is awaiting reconciliation, retain the snapshot and contact support; no automatic second debit is attempted. Older, unregistered snapshots also require support reconciliation.
Connected Chrome Controls
For MCP act_on_tab, pass settings: {llmIntegration: {model: 'gpt-6-luna', reasoningEffort: 'low'}}, disable_fallbacks: true (only that model at that reasoning level, or a MODEL_UNAVAILABLE error; see /docs/mcp#disable-fallbacks) and max_steps: 10 to control this invocation. Request overrides do not change saved extension preferences. For GMI inference only, set settings.llmIntegration.inferencePolicy to gmi. This uses managed GLM Flash, keeps normal billing, and refuses alternate-provider fallback. Cloud works through the existing service; connected Chrome requires an extension advertising MCP protocol 4 or later. An unavailable model fails before inference. max_steps limits actions; it is not a hard credit limit. Extension maxCredits is unsupported and rejected. Use planner/code-plan for createTool and enrich. For direct take_page_action, inspect actionResults, verified, updated trees and any network output/outputRef; a dispatched click or unchanged feed is not evidence of successful collection.
Generated webpage tools receive deadlineEpochMs, a host-owned deadline that includes navigation. Collect bounded batches and return progress before it. On timeout or cancellation, the runtime closes the tabs it created and preserves completed URLs in error.details.partialResults; use those results when resuming the remaining work. To report blocked or partial work, return rtrvr.done(result, {status: 'partial', reason: 'Feed stopped before completion'}); use failed when the requested work failed. These options set completion status without interpreting fields inside your records.
Cost & Credits
- 01
Free allowance: your first 1,000 records each month are free. Enrich records and web searches draw from one shared allowance, which resets on your account's renewal date.
- 02
Instant lookup: 0.25 credits per returned record beyond the allowance; zero matches cost nothing.
- 03
Live scrape: 0.15 credits per successful record beyond the allowance; failed inputs are free.
- 04
Discovery: listing datasets and inspecting fields is always free and never counts against the allowance.
- 05
Pending jobs: a scrape that exceeds the sync window bills nothing until you collect it.
- 06
Credits are billed at cost (1 credit = $0.01). Overflow beyond the free allowance bills automatically: there are no approval prompts.
Free Allowance & Billing
Enrichment runs without approval prompts. Every account gets 1,000 free records per month, shared between enrich calls and web searches, resetting on the account's renewal date. When a run exceeds what's left of the allowance, the overflow bills automatically at the per-record rates above. Each response reports creditsUsed and how many records the free allowance covered, so a plan can always tell you exactly what a run cost.
Example: Enrich LinkedIn Profiles
You do not have to write this yourself: ask in plain English (e.g. "enrich these LinkedIn profile URLs with name, title, and company") and the agent generates and runs the plan. The code below is what it runs under the hood.
// Build lookup keys from your sheet rows: match on the lowercase LinkedIn
// profile slug (the segment after /in/), NOT the full URL.
const ids = rows
.map(r => r.linkedinUrl?.split('/in/')[1]?.split(/[/?#]/)[0].toLowerCase())
.filter(Boolean);
// Instant lookup — no approval step needed: the first 1,000 records/month are
// free and overflow bills automatically (the response reports creditsUsed).
// Pass ALL ids in ONE `in` filter (batching is server-side).
const { records } = await rtrvr.enrich({
dataset: rtrvr.datasets.linkedinPeople,
filter: { name: 'id', operator: 'in', value: ids },
fields: ['id', 'name', 'position', 'current_company_name', 'city'],
});
// records → join back onto your rows on `id`, then write the enriched columns.Enriching LinkedIn URLs with Emails
To add emails and phone numbers, use the linkedinPeopleContact dataset: same instant lookup by id slug, but request the email and cellphone_number fields explicitly (they are dropped unless named). Coverage is partial: this is partner-sourced B2B contact data, and most profiles return null for both, so treat any email you get as a bonus rather than a guarantee. It is lookup-only: there is no scraper behind it, so scrapeUrls on linkedinPeopleContact is served by the linkedinPeople profiles scraper and returns profile fields but never email/cellphone_number: a person missing from the instant lookup cannot be recovered by a live scrape; report the miss.
const { records } = await rtrvr.enrich({
dataset: rtrvr.datasets.linkedinPeopleContact,
filter: { name: 'id', operator: 'in', value: slugs },
fields: ['id', 'name', 'position', 'current_company_name', 'email', 'cellphone_number'],
});
const filled = records.filter(r => r.email).length;
// e.g. "12 of 60 profiles had an email" — a blank email on a matched row is
// normal, not a failed lookup.Example: Enrich Companies by Domain
The linkedinCompanies dataset is instant-searchable and carries a website field, so a column of company domains can be enriched with firmographics in a sub-second lookup, no scraping required.
const { records } = await rtrvr.enrich({
dataset: rtrvr.datasets.linkedinCompanies,
filter: { name: 'website', operator: 'in', value: domains },
fields: ['name', 'website', 'industries', 'company_size', 'headquarters'],
});
// Domains are stored as full URLs, so `in` can miss on scheme or a leading
// "www.". If a domain returns nothing, retry it with the `includes` operator.Example: Enrich Amazon Products by URL
Non-LinkedIn datasets are live-scrape only: you supply each product's URL and get a fresh record back. Chunk lists larger than 20 with rtrvr.mapLimit.
const chunks = [];
for (let i = 0; i < productUrls.length; i += 20) chunks.push(productUrls.slice(i, i + 20));
const results = await rtrvr.mapLimit(chunks, 2, async chunk =>
rtrvr.enrich({
dataset: rtrvr.datasets.amazonProducts,
scrapeUrls: chunk,
fields: ['asin', 'title', 'brand', 'initial_price', 'availability'],
}),
);
// Each result may come back `pending` — collect it with { collect: snapshotId }.
// Join back onto your rows on `url` (or `input_url`, the URL you passed in).Web Search
Plans can also search the web programmatically with rtrvr.webSearch({ query, count?, gl?, hl?, recency?, since?, until?, news?, sortByDate? }): one Google query per call, returning parsed organic results ({ title, url, snippet, source?, date?, dateText? }) and, for place-flavored queries, localResults (Google's local pack: name, address, rating, reviews, category, and a canonical maps link). Phone numbers and websites are not in the pack: they come from each business's own site or listing. Bound a search to a time window with recency (hour, day, week, month, year) or with explicit since/until dates (YYYY-MM-DD), and add sortByDate to order newest first. A time-bounded search returns each result's publication date as an absolute, already-normalized date, so a plan never has to parse a phrase like 5 hours ago itself; appliedFilters states the window that was applied and notice states anything that could not be. A result with no date is undated, which never means recent. Date filtering runs on Google's news vertical, the only one that carries publication dates, so any date filter turns news on unless you explicitly pass news: false. Queries draw from the same 1,000-record monthly free allowance as enrichment, then cost a flat 0.15 credits per query with no approval prompt, and it replaces navigating a browser tab to Google. Localize with gl (country) and hl (language).
Recipe: Local-Business Lead Generation
The pieces compose into a lead-generation pipeline: ask in plain English (e.g. "find 100 businesses in Casablanca and Rabat that likely need a website") and the agent runs this shape:
- 01
Fan
rtrvr.webSearchover each industry × city (localized, e.g.gl: 'ma', hl: 'fr') and harvest the local pack: business name, phone, website (or its absence), rating, maps link. - 02
Construct a Google Maps link per business:
https://www.google.com/maps/search/?api=1&query=Name+Cityresolves to the real place page. - 03
Enrich businesses that have a website with a free fetch of their contact/about page (email, socials), or instantly via the
linkedinCompaniesdataset filtered onwebsite. - 04
Qualify with one LLM pass (
rtrvr.inferSheetData): e.g. a "why they need a website" column and a priority score. - 05
Rows append to a Google Sheet as they are found, so partial progress always survives; export the sheet as CSV when done.