Account
An Account is an organisation stored in LeadHunter. It might be a prospect, a customer, a partner, a reseller, a competitor, a journalist, an investor, a candidate, or part of your personal network — the same row serves all of those roles, with multi-select relationship types to label what each account is to you.
This is intentional. Most CRMs split “leads” and “customers” and “partners” into separate tables and then quietly accumulate duplicates across them. LeadHunter keeps one row per organisation and uses lifecycle + relationship type to describe context.
What an account carries
Section titled “What an account carries”The standard fields, grouped by what they describe:
| Bucket | Fields |
|---|---|
| Identity | name, business_type, specialization, language, logo_url, rating |
| Company profile | year_founded, employee_count_estimate |
| Location | address, city, state_province_region, postal_code, country, latitude, longitude, google_place_id |
| Contact | phone, email, website (plus a separate Contacts list of people inside the account — see below) |
| Social profiles | linkedin_url, instagram_url, x_url, facebook_url, youtube_url, tiktok_url |
| Lifecycle | status, status_changed_at, status_changed_by, status_source, status_history, external_contact_history |
| Classification | relationship_types, acquisition_channel, acquisition_metadata, acquired_at |
| Provenance | source (how the row entered the system), is_verified, notes, merge_history |
| Cached content | website_content (scraped text used by the AI), website_scraped_at, website_discovery_attempted |
| Custom | custom_fields — per-company schema; see Custom fields. |
Most fields have a sensible default and are optional. The only truly required field is name. Everything else can be populated incrementally — auto-enrichment fills website + content + language for free shortly after the row is created.
What happens after you add an account
Section titled “What happens after you add an account”When an account lands in the database — by import, lookup, Google Maps discovery, or one-off entry — LeadHunter queues a background job that:
- Discovers the website when only a name + location was provided. Uses Gemini grounding plus a domain-guessing fallback.
- Scrapes the discovered URL and stashes the text content on the account (
website_content). The cached content is what the scoring model reads later — it doesn’t re-fetch on every score. - Extracts structured fields from the scraped content — the company’s logo, phone, email, address, the per-platform social URLs, founding year, employee-count bucket, primary language, and the business name when the row arrived without one.
Auto-enrichment is idempotent at three levels — so manual edits and partial fills always survive a re-enrich:
- Row level: a row counts as already enriched when it has cached website content and the scrape is recent (within the cache window). Stale rows re-enter the queue automatically so the data doesn’t go cold; fresh rows are skipped entirely so a re-enrich doesn’t redo work.
- Field level: every structured field is only written when the current value is blank. If you manually corrected the LinkedIn URL or fixed a phone number, that value wins forever — re-enrichment can fill other blanks but never overwrites yours. The same rule applies to merge survivors: every field already populated from one of the absorbed rows stays put.
- Discovery level: when website discovery runs once and finds nothing, the row is flagged “discovery attempted” so subsequent passes don’t loop on the same dead-ends. You can still set a website manually at any time; the next enrich picks it up from there.
Wrong-kind URLs are auto-rediscovered. If the website value you imported turns out not to be a website at all — an audio stream URL from a radio export, a PDF link from a press pack, a download URL — enrichment now detects that on the first byte (no 15-second timeout, no wasted bandwidth) and tries to find the company’s real website automatically. When the stream carries a hint (Shoutcast/Icecast servers often include a homepage URL in their headers, for free), that hint wins. Otherwise the discovery agent runs and proposes a candidate URL, which is then verified to actually serve a webpage before the swap happens. The original URL is never lost — it lands in the account’s notes field with a small audit block explaining what kind of resource it was and why it got replaced, so you can always go back.
You’ll see an account grow website content, a language tag, socials, founding year, and a headcount bucket within minutes. Each row carries its own auto-tracked enrichment cost (USD, per model) so you can answer “how much did this account cost to fill in?” — exposed on the account detail. See Usage and costs.
You can also re-enrich on demand from the Enrich button on the Accounts page — three modes (only non-enriched / first 50 for a smoke test / currently filtered). The progress bar on the Tasks page reflects real work done, and large runs survive a redeploy without losing the chunks that already landed.
Company profile — year founded, employees, socials
Section titled “Company profile — year founded, employees, socials”Three structured signals beyond the free-form business_type make accounts easier to segment and easier to pitch.
year_founded— the year the business started, when known. Filterable in saved filters with the usual numeric operators (“founded since 2020”, “established 10+ years”). The auto-enrichment agent fills it when the website states it explicitly (“since 1985”, “founded in 2007”); otherwise it stays blank and you can enter it manually.employee_count_estimate— a coarse headcount bucket:1-10,11-50,51-200,201-1000, or1000+. The strongest single fit signal for B2B targeting, and the only headcount shape that survives across data sources (LinkedIn ranges, manual entry, scraped about-pages all map onto these five buckets). The scoring agent reads it when computing fit, so a 5-person shop and a 5,000-person enterprise won’t get scored against the same rubric.- Per-platform social URLs —
linkedin_url,instagram_url,x_url,facebook_url,youtube_url,tiktok_url. One URL per platform, filled by enrichment from the website’s<a href>links (the first canonical match wins; share-intent links like “share on Facebook” buttons are skipped). They render as clickable chips on the account detail page so jumping into a DM thread is one click away. Manual edits are preserved on re-enrich.
All seven are optional and skipped silently when the website doesn’t mention them. They’re available as targets in the CSV import column-mapper too — common headers (LinkedIn, IG, Twitter, Founded, Employees, Company Size, …) are auto-recognised.
These fields are different from the Contact-level social fields. A Contact’s linkedin_url and twitter_handle point at a specific person inside the account (the marketing director, the CEO); the Account-level URLs point at the company’s own profiles. Both layers can be populated at once and they describe different things.
Status — where in the pipeline
Section titled “Status — where in the pipeline”Each account has a single status (lifecycle state):
| Status | Meaning |
|---|---|
prospect | Default. Not yet contacted. |
contacted | You’ve reached out — at least one outbound message has been logged. |
in_negotiation | Active deal conversation. |
customer | They bought. |
lost | Deal fell through. |
do_not_contact | Hard opt-out. GDPR / CAN-SPAM stickiness — survives merges. |
Status transitions are recorded with their source — manual, pre_existing (see below), campaign_outreach (auto-promoted when you log a first outbound message), import_exact / import_fuzzy / import_llm_verified (set during import dedupe), or inbound_acquisition (auto-promoted when the account is created with an inbound acquisition channel). Every change appends to status_history, so you have a permanent audit trail of who set what when, and why.
Auto-promotion in one place
Section titled “Auto-promotion in one place”Status moves forward without operator action in two cases:
- Inbound channel on create — when an account is logged with an
acquisition_channelthat’s an inbound type (Adwords, Instagram DM, referral, …), it skipsprospectand starts atcontacted. The account has already reached out to you. - First outbound on a
prospect— when you log a first outbound message in any campaign, the account advances fromprospecttocontacted.
Recording a pre-existing customer
Section titled “Recording a pre-existing customer”Sometimes the account is already a customer when you add them to LeadHunter — a relationship that predates your funnel, an existing client you’re loading from a spreadsheet, or a long-standing renewal. You want them in the system as customer, but you don’t want the dashboard to claim them as a new win you just closed.
On the account page, when you change status to Customer, tick “Mark as pre-existing customer”. The transition is still saved with the full audit trail (you can see when and by whom), but the dashboard’s Closed (won) funnel skips it. Cost-per-customer math (CAC) skips it too — these accounts weren’t paid for by the campaigns you’re running today.
What counts as a win
Section titled “What counts as a win”The dashboard’s Closed (won) metric counts a status transition only when both are true:
- The account’s current status is
customerorlost. A transition that was later flipped back toprospectorcontactedis treated as a correction, not a real win, and drops out of the count. - The transition wasn’t tagged
pre_existing.
Click Closed (won) on the dashboard to jump straight to the matching accounts list, filtered to customer + lost.
Relationship types — what they are to you
Section titled “Relationship types — what they are to you”Orthogonal to status, an account can carry one or more relationship types (multi-select — one row can be both client and partner, or reseller and press).
The thirteen values, with their campaign-outreach guardrail policy:
| Type | Use when… | On bulk-add |
|---|---|---|
client | They pay you. | Allowed (customer lifecycle status separately gates outbound — see below). |
prospect | Sales target — the default classification on most rows. | Allowed. |
reseller | Sells your product downstream. | Allowed. |
affiliate | Sends you referrals on commission. | Allowed. |
partner | Strategic, co-marketing, integration. | Allowed. |
influencer | Has audience reach you want to borrow. | Allowed. |
supplier | You buy from them. | Soft block — flip include_supplier=true to override. |
investor | Current or potential investor. | Soft block — flip include_investor=true. |
press | Journalist, publication, podcaster. | Soft block — flip include_press=true. |
analyst | Industry analyst (Gartner-style). | Soft block — flip include_analyst=true. |
candidate | Hiring pipeline. | Soft block — flip include_candidate=true. |
competitor | Direct competition. | Hard block — never overridable. |
personal_network | Friends, family, ex-colleagues. | Hard block — never overridable. |
Two separate guardrails apply at bulk-add time too:
do_not_contact(lifecycle status) — blanket opt-out. Blocks every campaign regardless of goal. Hard, never overridable. Survives merges (GDPR / CAN-SPAM sticky).customer(lifecycle status) — soft block for sales-shaped goals. Flipinclude_customers=trueto re-engage existing customers in a campaign. (Customer-expansion / renewal / event / research / partnership goals admit customers by default — see Outreach reasons and targets.)
These rules only apply at campaign bulk-add. Logging an individual message to an account already in a campaign doesn’t re-run these checks — opt-outs are the exception: both the blanket do_not_contact status and a per-purpose opt-out matching the campaign’s goal are enforced at message-send time too, so an opt-out recorded after the account joined a campaign still blocks that campaign’s composer.
Per-purpose opt-outs
Section titled “Per-purpose opt-outs”Real-world consent is purpose-scoped. An account that opted out of sales emails may still be fine receiving research interview requests; a press contact who asked to be removed from your media list may still attend events. LeadHunter exposes this via do_not_contact_purposes — a list of campaign goals the account has opted out of, separate from the blanket do_not_contact status.
A customer who wrote in “please stop selling to me but we’d love to participate in your customer research” gets do_not_contact_purposes=['sales']. Sales campaigns are blocked from contacting them; research, customer-expansion, renewal, and event campaigns can still reach them.
Recording an opt-out is a click on the Per-purpose opt-outs panel in the Account detail page right column (between Relationship Status and Relationship Type). The panel renders current opt-outs as removable chips, has a goal-picker for new opt-outs (with source and reason fields), and shows the most recent audit entries inline. Behind it: POST /api/accounts/{id}/record-dnc/ writes the purpose to do_not_contact_purposes and appends an entry to dnc_history — same shape as status_history. The audit trail covers opt_out and opt_in events, with source (manual / inbound_request / import) and an optional reason. This is the GDPR / CASL paper-trail surface — keep it accurate.
The two layers compose: an account is blocked from a campaign if either status='do_not_contact' (blanket) or the campaign’s goal is in do_not_contact_purposes (purpose-specific). Neither layer is overridable from the bulk-add include flags.
Zero duplicates
Section titled “Zero duplicates”Accounts are deduplicated at the moment of creation. LeadHunter checks:
- Google Place ID (exact match → auto-merge, 100% confidence).
- Name + city (exact after normalisation — strips legal suffixes, punctuation, filler words → auto-merge, 95%).
- Phone (normalised, exact match → auto-merge, 90%).
- Website domain (normalised, exact match → auto-merge, 85%).
- Fuzzy name within the same city (≥85% similarity → suggested merge for human review).
Auto-merges at levels 1–4 happen silently. Level-5 fuzzy candidates surface under Accounts → Duplicates for human review.
When a merge runs, the survivor becomes the golden record: every unique field from every duplicate is preserved (non-generic email beats info@, the company’s own domain beats an aggregator URL, the longer scrape wins, custom-field arrays union, and so on). The survivor’s merge_history JSONB grows an entry recording the absorbed rows’ full snapshots — visible on the account detail page, so you can always read what was on each side before the merge.
Lifecycle status escalates and never demotes. do_not_contact survives every merge. The AI picks a canonical name when the duplicates disagree (“Joe’s Pizza” beats “Joe’s Pizza Restaurant LLC Holdings 2014”).
Merges aren’t reversible — the absorbed accounts are deleted at the end. The merge_history snapshot is an audit trail, not an undo button. See Merge duplicates for the full operator workflow.
Archive and delete
Section titled “Archive and delete”Two different ways to remove an account from your day-to-day view — pick by intent.
Archive soft-hides the account from default lists while keeping every reference intact: campaign rows, scores, conversations, status history, expenses, audit trail. Use it when the row still belongs in your history but you don’t want to see it any more — the business closed, merged into a competitor, pivoted out of your market, or simply stopped being relevant. You can attach a free-text reason when archiving (recommended — “business closed Q1 2026”, “merged into X”, “out of geography”), which is visible on the account header and in the audit trail. Archived accounts can be unarchived at any time, with a single click, no data loss.
A common case for archive: the lifecycle status fields (lost, do_not_contact) describe intent toward the account — we tried to sell and didn’t close, or they opted out. Archive describes the account’s own state — the organisation no longer exists or is no longer relevant. Both can be true at once (you can archive a do_not_contact account if the business also closed), and the two flags are independent.
Delete permanently removes the row and cascades to every campaign row, score, conversation, contact, and expense reference. There is no undo. The Delete action on the account detail page requires you to type the exact account name to confirm — the typo gate is intentional, since the operation can’t be reversed. Use this only when you’re sure the row should never have existed (a test row, a manual data-entry mistake that the merge flow can’t fix). For everything else — even “definitely not coming back” — archive is the right answer.
Both actions live in the Danger Zone card at the bottom of the account detail page.
Acquisition channel — where the lead came from
Section titled “Acquisition channel — where the lead came from”Each account also carries an acquisition channel — the marketing channel that brought them in: outbound (default — you found them), adwords, meta_ads, linkedin_ads, organic_search, organic_social (Instagram DM, X, …), referral, event, partner, cold_inbound, or other.
Inbound channels (everything except outbound and other) automatically start the account at contacted instead of prospect, because they’ve already reached out to you. The change is recorded in the audit trail with the channel as the reason, so it’s clear why the lifecycle skipped a step.
Channel-specific details — UTM parameters, ad campaign id, Instagram thread URL, the referrer’s email — go in acquisition metadata, a free-form key/value bag attached to the account.
See Track inbound leads for examples on logging Adwords clicks, Instagram DMs, and referrals.
Custom fields and segmentation
Section titled “Custom fields and segmentation”Your business probably tracks things that don’t fit standard fields — store size, vehicle count, license tier, contract expiry, whatever. Define them once as custom fields on your company; they then appear on every account and can be used in saved filters and campaign targeting.
Contacts live inside accounts
Section titled “Contacts live inside accounts”An Account is the organisation. A Contact is a person sitting inside it — first / last name, email, phone, mobile, job title, department, seniority (c_level, vp, director, manager, senior, entry), LinkedIn URL, twitter handle, plus flags for primary contact and decision-maker. One account can have many contacts (or zero if you’ve only logged the org so far).
The two are different things — don’t confuse them. Filters, scoring, and campaigns all operate on Accounts. Outbound messages target a chosen Contact within the Account (via the campaign-account’s contact_* fields), so the person you’re emailing is per-campaign even though the organisation is global.
One Contact per account can be flagged primary. The primary contact is the default target for the conversation log when no other choice has been made for a campaign.
Notes are operator-only
Section titled “Notes are operator-only”The notes field on an account is operator-only — the AI doesn’t read it during scoring or message drafting. Use it for things you want your team to see but not the model: pricing context, internal politics, “Maria says don’t email after 6pm”.
If you want a piece of context to influence scoring, put it in a custom field or in the account’s business_type / specialization — those are fed into the scoring prompt. If you want it to influence outbound drafting, put it in the conversation history (every prior message is read when AI Continue runs).
Status, types, and channel — what each one answers
Section titled “Status, types, and channel — what each one answers”Three orthogonal fields describe an account’s place in your world, and it’s worth keeping them straight:
| Field | Answers | Single or multi? |
|---|---|---|
status | Where in the sales pipeline? | Single |
relationship_types | What kinds of relationship does this account have to me? | Multi |
acquisition_channel | How did this lead first reach me? | Single |
A customer (status) tagged as both client and reseller (relationship_types) that arrived via adwords (acquisition_channel) is a perfectly coherent row. Each field answers a different question; none of them subsumes the others.
Which campaigns this account is on
Section titled “Which campaigns this account is on”The account detail page has a Campaigns card listing every campaign this account is part of. Each row shows the campaign name, its goal, and where the account stands in that campaign (review status and outreach status), with archived campaigns sorted to the bottom.
There are two links per row:
- The campaign name opens the campaign.
- The Outreach button jumps straight into that campaign’s outreach inbox with this contact already open — so you go from “looking at the account” to “writing the next message” in one click, instead of opening the campaign, finding the right tab, and scrolling to the row. The deep link opens the contact even when it’s filtered out of, or sits past, the inbox’s current page.
If the account isn’t in any campaign yet, the card says so and points you toward adding it to one. See Campaign for how accounts move into outreach, and Log a conversation for working a thread.
Read next
Section titled “Read next”- Campaign — how accounts move from “in the database” to “in active outreach”
- ICP and scoring — how the per-product fit score is computed
- Merge duplicates — the operator workflow when LeadHunter flags candidates