Skip to content

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.

The standard fields, grouped by what they describe:

BucketFields
Identityname, business_type, specialization, language, logo_url, rating
Locationaddress, city, state_province_region, postal_code, country, latitude, longitude, google_place_id
Contactphone, email, website (plus a separate Contacts list of people inside the account — see below)
Lifecyclestatus, status_changed_at, status_changed_by, status_source, status_history, external_contact_history
Classificationrelationship_types, acquisition_channel, acquisition_metadata, acquired_at
Provenancesource (how the row entered the system), is_verified, notes, merge_history
Cached contentwebsite_content (scraped text used by the AI), website_scraped_at, website_discovery_attempted
Customcustom_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.

When an account lands in the database — by import, lookup, Google Maps discovery, or one-off entry — LeadHunter queues a background job that:

  1. Discovers the website when only a name + location was provided. Uses Gemini grounding plus a domain-guessing fallback.
  2. 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.
  3. Detects the site’s language and writes it to language so outbound messages render in the right one.

Auto-enrichment is idempotent:

  • Rows that arrive already-populated (a Google Maps result with website + phone + address) skip the enrichment.
  • Rows produced by a merge skip — they’re already populated from the source rows.
  • Rows where discovery failed once (website_discovery_attempted=true but still no website) don’t retry on every save.

You’ll see an account grow website content + a language tag 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.

Each account has a single status (lifecycle state):

StatusMeaning
prospectDefault. Not yet contacted.
contactedYou’ve reached out — at least one outbound message has been logged.
in_negotiationActive deal conversation.
customerThey bought.
lostDeal fell through.
do_not_contactHard opt-out. GDPR / CAN-SPAM stickiness — survives merges.

Status transitions are recorded with their sourcemanual, 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.

Status moves forward without operator action in two cases:

  • Inbound channel on create — when an account is logged with an acquisition_channel that’s an inbound type (Adwords, Instagram DM, referral, …), it skips prospect and starts at contacted. 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 from prospect to contacted.

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.

The dashboard’s Closed (won) metric counts a status transition only when both are true:

  • The account’s current status is customer or lost. A transition that was later flipped back to prospect or contacted is 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:

TypeUse when…On bulk-add
clientThey pay you.Allowed (customer lifecycle status separately gates outbound — see below).
prospectSales target — the default classification on most rows.Allowed.
resellerSells your product downstream.Allowed.
affiliateSends you referrals on commission.Allowed.
partnerStrategic, co-marketing, integration.Allowed.
influencerHas audience reach you want to borrow.Allowed.
supplierYou buy from them.Soft block — flip include_supplier=true to override.
investorCurrent or potential investor.Soft block — flip include_investor=true.
pressJournalist, publication, podcaster.Soft block — flip include_press=true.
analystIndustry analyst (Gartner-style).Soft block — flip include_analyst=true.
candidateHiring pipeline.Soft block — flip include_candidate=true.
competitorDirect competition.Hard block — never overridable.
personal_networkFriends, 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. Flip include_customers=true to 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 — do_not_contact is the only exception, enforced at message-send time too.

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.

Accounts are deduplicated at the moment of creation. LeadHunter checks:

  1. Google Place ID (exact match → auto-merge, 100% confidence).
  2. Name + city (exact after normalisation — strips legal suffixes, punctuation, filler words → auto-merge, 95%).
  3. Phone (normalised, exact match → auto-merge, 90%).
  4. Website domain (normalised, exact match → auto-merge, 85%).
  5. 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.

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.

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.

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.

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:

FieldAnswersSingle or multi?
statusWhere in the sales pipeline?Single
relationship_typesWhat kinds of relationship does this account have to me?Multi
acquisition_channelHow 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.

  • 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