Import accounts
There are three ways to get accounts into LeadHunter. All three run the same dedupe checks against your existing database, so you can’t accidentally create a second row for an account you already have — and the merge keeps every unique field from every duplicate rather than dropping data on the floor.
Pick the path that matches the source:
| Source | Use | Where |
|---|---|---|
| One-off entry | Quick add by name, URL, or search query | Accounts → Lookup |
| Existing list (CSV / XLSX) | Bring over a sheet you already maintain | Accounts → Import |
| New market discovery | Sweep a geo/category in one pass | Accounts → Discover (Google Maps) |
1. Single-account lookup
Section titled “1. Single-account lookup”Best for ad-hoc additions — “there’s this one bike shop I want to track, can you find it for me?”
From Accounts → Lookup, paste any of:
- A website URL (
https://acme.com) — scraped + enriched. - A bare domain (
acme.com) — LeadHunter prependshttps://. - A Google Maps URL — pulls address, phone, hours, rating, place id.
- A plain-text query (“bike shops in Berlin”) — Google Maps text search; the top match is used. Requires Google Maps to be configured.
Three research modes
Section titled “Three research modes”Each lookup runs in one of three modes — the dropdown next to the URL field:
| Mode | What runs | When to pick |
|---|---|---|
| None | Plain scrape; no LLM. | You’re importing a vetted URL and don’t want to spend Gemini quota. |
| Quick (default) | AI extracts business name, sector, language, key fields you didn’t provide. | Most of the time. |
| Deep | Quick + multi-page crawl (about / team / staff / contact pages) + decision-maker contact extraction. | When you want named contacts pre-filled, not just the company. |
Deep mode is heavier (more page fetches, more tokens) but produces an account that’s already populated with one or two contacts with names, titles, and (when discoverable) emails. Use it for high-value accounts where you’d otherwise hand-research the buying group.
2. CSV / XLSX bulk import
Section titled “2. CSV / XLSX bulk import”Best for bringing over a list you already maintain — your CRM export, a trade-show booth scan, an industry directory you bought, etc. Accounts → Import runs a three-step wizard.
Step 1: Upload
Section titled “Step 1: Upload”Drop a .csv, .xlsx, or .xls file. LeadHunter parses the headers and shows you a sample of the first few rows so you can confirm parsing worked (Excel files with ragged rows — rows that have fewer cells than the header — are handled correctly).
Step 2: Map columns
Section titled “Step 2: Map columns”This is the step where you tell LeadHunter what each column means. The AI proposes a mapping based on the column names and the sample rows — e.g. org_name → name, tel → phone, country_iso → country. Review and adjust.
Standard target fields:
name, address, city, state_province_region, postal_code, country, phone, email, website, business_type, specialization, rating, language, status, notes, acquisition_channel, latitude, longitude, google_place_id, imported_id.
Aliases — state, province, and region all resolve to state_province_region, so you don’t have to rename your column.
Skip — set the target to skip for columns you don’t want imported. Skipped columns can still be preserved as JSON (see Save extras below).
imported_id — map your source’s stable row id here. If your file has a column like Station UUID, CRM Record ID, Customer ID, or any other identifier that uniquely identifies a row in the source dataset, map it to imported_id. Re-importing the same file (or a new export from the same source) will then resolve every match via that id at the highest confidence — even when name + city don’t agree, or are missing entirely. Without an imported_id, dedupe falls back to name + city + phone + website + fuzzy name, which can miss matches when the file is sparse (radio-station directories, lead-magnet form dumps, etc.). Common header names that the AI auto-suggests: ID, External ID, OriginalID, Imported ID.
name is mandatory — the import refuses to start without at least one column mapped to it.
Already imported this file? You’ll see a warning.
Section titled “Already imported this file? You’ll see a warning.”If the bytes of the file you just uploaded exactly match a previous import in this company, the mapping step shows a yellow banner at the top: “You’ve already imported this exact file” — with the prior import date, who ran it, how many rows landed, and how many of those accounts still exist. Cancel if you uploaded the wrong file; proceed if the warning is just informational (you re-uploaded on purpose). The dedupe stack will skip the duplicates regardless.
Step 3: Run (and the options worth knowing)
Section titled “Step 3: Run (and the options worth knowing)”A few options control how the import behaves:
- Static values — constants applied to every row. Useful when the file doesn’t carry a column for it but the value is the same for the whole batch. Examples:
country=Spainfor a Spanish trade-show export,language=Catalanfor a regional directory,acquisition_channel=eventfor a booth scan. Anything you set here overrides values from the file. - Save unmapped columns as
extra_data— when on, every column you mapped toskip(or didn’t map at all) is preserved as a JSON dict on each account. Use this when the source has fields LeadHunter doesn’t model but you don’t want to lose them. - Enrich accounts after import — when on, every newly-created account is queued for website discovery + scrape (logo, phone, email, address, the per-platform social URLs, year founded, employee-count bucket, business language, and the business name when the row arrived without one) once the rows land. The enrich runs as a separate background job that shows on the Tasks page. Off by default — it costs Gemini calls and time. Tick it for a small trusted list; leave it off for a 50k-row directory unless you actually want every row enriched. You can always enrich later from Accounts → Enrich (see After the import: enrichment).
- Source label — a free-text tag stamped on each imported row’s
sourcefield. Defaults to the filename (lower-case, with separators tidied —radio_stations_radiobrowser.xlsxbecomesradio stations radiobrowser). Edit it to whatever provenance marker reads best to you and your teammates:"LinkedIn export 2026-Q2","Trade show 2026","Stripe customers", etc. Beyond the label, every imported row also carries the file’s MD5 fingerprint invisibly, so you can later answer “show me every account that came from this specific upload” exactly even if multiple imports share the same label. - Dry-run — runs every dedupe check and shows you what would happen (created / merged / fuzzy candidates) without writing. Recommended for any import larger than a handful of rows.
Big files run in the background
Section titled “Big files run in the background”Files above about 2,000 rows are handed to a background worker so the browser doesn’t sit on the request. You’ll see a live progress bar with rows-processed / total, animating as the import advances:
- You can close the dialog at any time — the import keeps running on the server, and the accounts show up on Accounts as soon as it finishes.
- The worker imports rows in chunks of 2,500, each in its own transaction. If a single row in chunk N gets rejected by the database (e.g. a column type that doesn’t fit), only that chunk rolls back — the chunks that already committed stay in place. You can fix the offending rows in the source file and re-import just them, instead of losing 50,000 good rows because of 100 bad ones.
- The job also shows on the Tasks page as a row of method CSV Import. Click it for full details (timing, totals, error log).
Smaller files (and any dry-run) finish synchronously — same modal, no progress step. Smaller imports also run as a single transaction (no chunking), since one rejected row in a 200-row file is small enough to retry whole.
What the result panel shows
Section titled “What the result panel shows”When the real import finishes, the summary tells you: rows created, rows merged into existing accounts, fuzzy-match candidates that need your review, and rows skipped (with reasons). Fuzzy candidates land in Accounts → Duplicates for confirmation.
For chunked imports, a small footnote lists how the file split — e.g. “Imported in 21 chunks of 2,500 rows. 19 committed, 2 rolled back.” If everything went well, all chunks committed and the import is complete. If some chunks fail (bad rows in only part of the file), the import is partially complete — the good chunks landed, the bad ones didn’t, and the result panel shows which row numbers tripped the database so you can fix them.
If LeadHunter had to truncate any over-long values to fit the column limits (Name is capped at 255 characters, Website at 500, Language at 50, etc.), an amber summary box lists the count per field — e.g. language: 126 · name: 17 · website: 2. The rows still imported; you just lost the tail of those long strings. Fix the source file if the truncated values matter.
If every chunk rolled back (the source file is broken end-to-end), the panel turns red, says “Import failed — nothing saved”, and lists the underlying database errors. Nothing lands; fix the source and retry.
Undo this import
Section titled “Undo this import”Picked the wrong file by accident? On the result panel, the Undo this import button deletes every account that landed from this file in one click — plus any campaign memberships, conversation messages, and scores that got attached to those accounts in the meantime. The import history record stays so you have an audit trail, but the rows are gone.
It uses the file’s MD5 fingerprint (stamped invisibly on every imported row) to find exactly what to delete, so it never reaches into accounts that came from a different upload or were entered manually.
The undo is destructive and cannot be reversed. If you imported a 50k-row CRM export, ran outreach campaigns against some of those rows, and then click Undo, those campaign rows and the messages you sent are gone too. Most operators want Undo right after the import lands (“oops, wrong file”); after that point, fixing duplicates one-by-one in Accounts → Duplicates is usually safer than wholesale revert.
3. Google Maps discovery
Section titled “3. Google Maps discovery”Best for entering a new market — “who are all the bike shops in Berlin?” Run a query from Accounts → Discover and LeadHunter pages through Google Maps results, fetches the place details for each, and adds them as accounts.
Each result runs through the same dedupe stack, so re-running the same query a month later doesn’t double-count — it merges fresh place details (new phone, updated hours, current rating) into the row you already have. The google_place_id constraint makes the match exact.
Requires Google Maps API to be configured on your environment; ad-hoc lookups by Maps URL or domain don’t depend on it.
After the import: enrichment
Section titled “After the import: enrichment”LeadHunter can run an enrichment pass on every freshly-imported account that:
- Tries to discover the website via Gemini grounding (and a domain-guessing fallback when the model has nothing).
- Scrapes the discovered URL and caches the text content on the account.
- Extracts structured fields from the page: logo, phone, email, address, the per-platform social profile URLs (LinkedIn / Instagram / X / Facebook / YouTube / TikTok),
year_founded, the coarseemployee_count_estimatebucket, primary business language, and the business name when the row arrived without one.
Three ways this gets triggered:
- Single-account lookup (Quick / Deep modes) runs the enrich inline — it’s part of the lookup itself.
- Bulk import (CSV / XLSX) — tick Enrich accounts after import during step 3. The enrich runs as a separate background job once the rows land. Off by default so you can decide; a 50k-row directory imported without the box ticked stays cheap and fast, and you can enrich a subset later from the Enrich button on the Accounts page.
- Google Maps discovery rows arrive already-populated from the Maps result (website + phone + address), so the enrich is usually a no-op and is skipped.
Enrich already-imported accounts
Section titled “Enrich already-imported accounts”The Enrich button on the Accounts page (next to Import, Export, Find Duplicates) opens a small dialog with three modes:
- Only non-enriched accounts (default) — skips every account that already has scraped website content. Recommended for project-wide enrichment.
- First 50 (test run) — caps the run at the 50 oldest non-enriched accounts. A cheap smoke test before kicking off a multi-hour pass on a large project.
- Currently filtered accounts — applies the page’s current filter set (search, status, country, business type, advanced filters …) and enriches only those rows. Useful when you want to enrich a specific country slice or every “prospect” before a campaign launch.
Whichever mode you pick, the run lands on the Tasks page with a real live progress percentage — not the placeholder it used to park at. Large jobs (>200 accounts) are sliced into chunks server-side so the percentage advances steadily instead of jumping from 0% to 100% at the end, and so a redeploy mid-enrich doesn’t lose the work that already landed (it picks up where it left off automatically — see below).
How “already enriched” is decided
Section titled “How “already enriched” is decided”LeadHunter tracks enrichment state at three independent levels, so manual edits and partial fills always survive a re-enrich:
- Row level — should we scrape at all? A row counts as already enriched when it has cached website content and the scrape is recent. Stale rows (older than the scrape cache window) re-enter the queue automatically.
- Field level — should we overwrite? Every structured field (phone, email, the social URLs,
year_founded,employee_count_estimate, …) is only written when the current value is blank. So if you manually corrected the LinkedIn URL on one account, a re-enrich never overwrites it. - Discovery level — did we already try? When website discovery fires (Gemini grounding + domain guessing) and finds nothing, the row is flagged “discovery attempted”. Subsequent runs skip it so we don’t loop on the same dead-ends.
You’ll see freshly-imported accounts grow website content, a language tag, socials, founding year, and a headcount bucket over the following minutes. The activity is tracked under API costs and visible on the Tasks page.
Deploy-safety: large enrich jobs survive a redeploy
Section titled “Deploy-safety: large enrich jobs survive a redeploy”Enrichment runs that take longer than a worker’s lifetime (long imports, big project-wide enriches) used to risk losing in-flight work if the platform redeployed mid-run. The current pipeline:
- Slices large jobs into chunks that each finish well within a worker’s time limit.
- Persists the chunk plan so a worker that picks up after a restart knows exactly which slices already landed and which still need to run.
- Auto-resumes orphaned runs when a fresh worker comes online and once an hour on a steady cadence. A job that was interrupted continues from where it left off; you don’t need to click Retry.
If a run can’t be auto-resumed (a CSV import whose upload-cache expired, for example), it’s flagged failed on the Tasks page with a one-line explanation and a Retry button.
Deduplication, in order
Section titled “Deduplication, in order”Every path runs the same dedupe stack. The first level that matches wins:
- Google Place ID — exact match → auto-merge (100% confidence). The hardest constraint; a Google place is unambiguous.
- Imported ID — exact match on
imported_idwithin the same company → auto-merge (100% confidence). When you mapped the source’s stable row id (Station UUID, CRM Record ID, etc.) during the import, a re-import resolves here first — even when name + city don’t agree or are blank. This is the level that makes monthly CRM re-exports clean: every row that already exists is found here. - Name + city — exact match after normalisation (legal suffixes, punctuation, filler words stripped). Auto-merge at 95%.
- Phone — normalised exact match (strips formatting, country codes, spaces). Auto-merge at 90%.
- Website domain — normalised exact match (
www., scheme, trailing slash stripped). Auto-merge at 85%. - Fuzzy name within the same city — ≥85% similarity. Suggested merge — surfaces in Accounts → Duplicates for you to confirm.
When a candidate is auto-merged (levels 1–5), every unique field from every duplicate is preserved on the survivor (the “golden record”), and the audit trail records what was merged. When a candidate is suggested (level 6), it shows up in Accounts → Duplicates for you to confirm or reject — LeadHunter never auto-merges fuzzy candidates.
For the full merge workflow + field-winner rules, see Merge duplicates.
Common pitfalls
Section titled “Common pitfalls”- Importing without dry-run. For anything over ~50 rows, run dry-run first. It costs nothing and tells you exactly what the dedupe stack will do — including which rows will merge into existing accounts.
- Skipping the column mapping review. The AI suggestion is usually right, but it’s worth a scan — a
notescolumn that lands ondescriptionand vice versa is annoying to clean up later. - Forgetting to set
acquisition_channelfor inbound imports. If you’re loading a Stripe-customer export or an Adwords-leads CSV, setacquisition_channelas a static value (adwords,cold_inbound, etc.) so the rows land atstatus='contacted'and slot into the attribution dashboard instead of looking like outbound prospects. - Re-importing a file you’ve already imported. LeadHunter detects this — the mapping step shows a yellow warning with the prior import’s date, who ran it, and how many rows are still around. The dedupe stack then skips every match, so re-imports of unchanged files are safe no-ops. The trap is when the file changed (new export from the same CRM) but you didn’t map a stable id: without
imported_id, dedupe falls back to name + city + phone + website, which can miss matches on sparse files and create duplicates. Map the source’s stable id toimported_idfor any dataset you’ll re-import regularly.
Read next
Section titled “Read next”- Merge duplicates — confirm and resolve fuzzy candidates LeadHunter flagged.
- Track inbound leads — attribute imports from Ads / forms / referrals correctly.
- Build a saved filter — once accounts are in, slice them into campaign-ready lists.