What's new
A running log of changes you can feel in the product. Most recent first.
July 2026
Section titled “July 2026”Client Pulse — stay close to your existing clients
Section titled “Client Pulse — stay close to your existing clients”Client Pulse is LeadHunter’s answer to “we won the deal… now what?”. It closes the loop after an account becomes a customer:
- Usage signals from your own products. Your product’s backend sends LeadHunter a tiny ping when a client opens the app, uses a feature, starts a trial, or converts — authenticated with a simple per-company key. LeadHunter shows you, per client, when they were last active and which features they actually use.
- A new “In trial” lifecycle stage between In negotiation and Customer, driven automatically by those pings (or set manually).
- A contact cadence you configure per company — for example
“talk to every client at least once every 6 weeks”. Clients you
haven’t contacted within the cadence surface on the Clients
page, in a weekly email digest, on the dashboard home, and
(optionally) through the
client.contact_dueoutbound webhook so you can route the nudge to Slack or your own tools.
The combination is the point: heavy product usage with no recent contact is an upsell conversation waiting to happen; silence on both fronts is churn risk you can still act on. Full walkthrough: Stay close to your clients — Client Pulse.
Invitation emails and the Sign up link work again
Section titled “Invitation emails and the Sign up link work again”Team-invitation emails and the login page’s Sign up link pointed at a page that no longer existed, so new teammates hit a dead end. Both now land on the real account-creation page. If you invited someone recently and they couldn’t register, re-send the invite from Settings → Team (revoke the pending one first — re-inviting does this automatically).
The sidebar shows your real API spend
Section titled “The sidebar shows your real API spend”The sidebar footer used to show a placeholder credits counter. It now shows the active company’s actual month-to-date API spend, and when you’ve set a monthly budget it becomes a progress bar toward it (amber once you cross your alert threshold). Click it to open the full Usage page.
Reliability and data-safety round-up
Section titled “Reliability and data-safety round-up”A batch of fixes from a full review of the platform, all live now:
- Sturdier CSV exports — exported cells can no longer be misread as spreadsheet formulas by Excel, Google Sheets, or LibreOffice, so opening an export is always safe.
- Duplicating a campaign keeps its setup — the copy now preserves the goal, scoring brief, outreach brief, and communication language instead of resetting them to defaults.
- Merging accounts preserves everything — conversation history now moves to the surviving account, and every newer field (opt-out purposes, relationship types, custom fields, social links, captured value, and more) carries over instead of being dropped.
- Clearer errors on bad filter values — a typo in a filter or a malformed value now returns a clear validation message instead of a failed request.
- Import wizard keeps your file after a failed run — if an import fails, you can fix the mapping and retry without re-uploading.
- Clear message for legacy
.xlsfiles — the import wizard now tells you up front to save as.xlsxor CSV instead of failing with a generic error. - Team management hardened — the older team endpoints were removed; all member and role changes now go through the owner-controlled Settings → Team flow.
June 2026
Section titled “June 2026”Override the translation languages per conversation
Section titled “Override the translation languages per conversation”The From → To direction at the top of the composer is now editable. It still pre-fills from your resolved languages (account → campaign → product → you → English), but you can change either side for the conversation in front of you — type any language or pick one from the dropdown, and a swap button flips the direction. It steers both the AI drafts and type-&-translate for that conversation only and isn’t saved; a reset link puts the resolved languages back. Handy when a lead’s detected language isn’t quite right for one message. For a durable change, edit the account/campaign/product/your language as before. See Messaging → Override the languages for one conversation.
Invite your team — self-serve
Section titled “Invite your team — self-serve”No more emailing support to add a teammate: Settings → Team now handles it. Invite by email (existing users join instantly; new ones get an invitation that auto-attaches when they sign up with that email), change roles, remove members, revoke pending invites — with the last-owner guard keeping every company administrable. Invitations expire after 14 days. See Team and companies.
Budget alerts — know before the bill does
Section titled “Budget alerts — know before the bill does”Set a monthly API budget on the Usage page and LeadHunter watches it for you: a progress bar tracks month-to-date spend, and the whole team gets an email when spend crosses your threshold (default 80%) and again at 100% — once per level per month. It’s an early-warning system, not a cut-off. See Usage and costs → Budget alerts.
The funnel now shows who’s clicking
Section titled “The funnel now shows who’s clicking”The dashboard funnel gained a Clicked stage between Initiated and Responded, fed by tracked links: how many contacts clicked something you sent, before any reply. It comes with its own daily chart, a cohort click-through rate alongside the response and close rates, and a clicked column on the per-product / per-campaign stats tables. Bot clicks stay filtered out, and the maturity caveat applies as with the other cohort rates — fresh cohorts haven’t had time to click yet.
Close the loop: push data back into LeadHunter
Section titled “Close the loop: push data back into LeadHunter”The integrations story is now round-trip. POST /api/accounts/upsert-by-imported-id/ lets your CRM or automation tool push state into LeadHunter keyed on the same imported_id the webhook payloads carry: only the fields you send change, custom fields merge per key, unknown accounts are created, and status changes run through the normal pipeline (audit trail + webhooks fire, repeated pushes are no-ops). Webhook out, upsert in — see Push events to your tools → Pushing data back in. Webhook delivery history is now kept 90 days and pruned automatically.
Webhooks — LeadHunter now pushes events to your tools
Section titled “Webhooks — LeadHunter now pushes events to your tools”Connect your CRM, Zapier, n8n, Make, or your own backend: Settings → Webhooks lets you register HTTPS endpoints that receive account.created, account.status_changed, and message.inbound events the moment they happen. Deliveries are HMAC-signed (verify with your per-webhook secret), retried with backoff, replayable one click at a time from the delivery log, and a dead endpoint switches itself off after 20 straight failures instead of retrying forever. Payloads carry your own imported_id, so events arrive pre-matched to your CRM records. A built-in Test button fires a synthetic event through the real pipeline. See Push events to your tools.
One Usage page for every cost
Section titled “One Usage page for every cost”The long-promised Usage page is live (sidebar → Usage): this-month / last-30-days / all-time API spend, a 90-day daily trend, 30-day breakdowns by model and by provider, the top campaigns and tasks by AI spend (each linking straight through), and your user-entered campaign + channel expenses per currency — all side by side, per company, in one view. Auto-tracked costs are in USD; manual expenses stay in the currency you entered them (no silent FX). See Usage and costs.
Tracked links — see who clicks before they reply
Section titled “Tracked links — see who clicks before they reply”Every link you paste into outreach can now be a tracked short link. Create one from the new Tracked links panel in the conversation pane (it pre-fills with the account’s website), copy it into your message, and LeadHunter records every click — the conversation shows the count and the inbox row gets a clicks chip, so a contact who clicked twice yesterday floats to the top of your attention even though they haven’t replied yet. Link-preview bots are filtered out of the counts, no personal data is stored about the clicker, and destinations are immutable once created so the click record stays honest. See Messaging → Tracked links.
Outreach inbox: load more rows, steadier progress bars
Section titled “Outreach inbox: load more rows, steadier progress bars”A batch of quality-of-life fixes across the campaign pages:
- The outreach inbox’s contact list now has a Load more button instead of stopping at the first 50 rows — page through a big campaign without narrowing the search.
- The scoring page’s progress bar no longer freezes on long runs (it used to stop updating after 10 minutes even though scoring kept going), and a failed approve/reject now tells you instead of silently doing nothing.
- The campaigns list’s live scoring banner now sees runs on any page of the list — previously a run on page 2, or one hidden by an active search, showed no banner.
- Fast filter/tab switching can no longer briefly show stale results in the outreach inbox.
May 2026
Section titled “May 2026”Install LeadHunter as an app
Section titled “Install LeadHunter as an app”LeadHunter is now an installable web app. Install it from Chrome / Edge on desktop, Add to Home screen on Android, or Share → Add to Home Screen in iOS Safari, and the dashboard runs in its own window with its own icon — no browser tabs to lose, faster loads. When a new version ships, the app updates itself in the background and offers a one-click Reload. Step-by-step instructions per platform: Install LeadHunter as an app.
Outreach inbox: search notes, and share a link to a conversation
Section titled “Outreach inbox: search notes, and share a link to a conversation”Two quality-of-life upgrades to the campaign outreach inbox:
- The search box now also matches the internal note and AI draft directions on each row — not just names and emails. Stash an order number, CRM id, or reminder in a contact’s note and you can jump straight to them by searching it.
- Opening a conversation now updates the URL. Refresh and you stay on the same thread; copy the link and send it to a teammate and they land right on that account’s conversation. No more losing your place or describing “the third one down”.
Three AI drafts to choose from — with one-click copy
Section titled “Three AI drafts to choose from — with one-click copy”Hit Draft with AI and you now get three drafts, each a different approach — a direct one-liner, a warm relationship-first opener, an outcome/value pitch — each labelled with its angle and a one-line “why”. Pick the register that fits the account instead of wrestling a single draft into shape. Every draft has a Copy button (puts the exact text on your clipboard to paste into your channel — the normal flow, since LeadHunter logs rather than sends) and a Use this button (loads it into the editable box to tweak and log). The first approach is selected automatically, and Redraft gives you three fresh angles. See Messaging → Three approaches, pick one.
Tell the AI how to write your outreach
Section titled “Tell the AI how to write your outreach”You can now hand the AI drafter directions in your own words, at two levels. A campaign-wide AI outreach brief (a panel at the top of the campaign’s outreach inbox) applies to every draft in the campaign — “keep it under 60 words, friendly, no emojis; lead with the integration angle”. And a per-account AI draft directions box (at the top of each conversation) handles one-off context for a single account — “they demoed last week, reference it; don’t bring up pricing yet”. Per-account directions layer on top of the campaign brief and win on any conflict. Both save as you click away and take effect on the next draft — nothing to regenerate. They’re distinct from the internal note, which the AI never reads. See Messaging → Steering the draft.
See an account’s campaigns — and jump straight into the conversation
Section titled “See an account’s campaigns — and jump straight into the conversation”The account detail page now has a Campaigns card listing every campaign the account is part of, with each campaign’s goal and where the account stands in it (review + outreach status). Two links per row: the campaign name opens the campaign, and an Outreach button jumps straight into that campaign’s outreach inbox with this contact already open — so going from “looking at the account” to “writing the next message” is one click, no hunting through tabs and pages. The deep link finds the contact even when it’s filtered out of or sits past the inbox’s current view. See Account → Which campaigns this account is on.
Custom fields that deep-link into your other tools
Section titled “Custom fields that deep-link into your other tools”A url custom field can now carry a deep-link template — a URL with {placeholders} LeadHunter fills in per account — so one click jumps from an account straight into your CRM, billing system, or admin panel. Set it up in Settings → Custom fields: choose type URL, write something like https://crm.example.com/contact/{imported_id}, and give it a link label (e.g. “Open in CRM”). Placeholders can be a value you type per account ({value}), another custom field ({cf.<key>}), or the account’s own data — and the standout is {imported_id}, which deep-links every imported account back into its source system with zero extra typing. Links open in a new tab, are checked to be safe web addresses, and show up in CSV exports as the finished URL. You can also now scope a custom field to one product so product-specific fields stay grouped. (Two-way sync — pushing data out on account events — is on the roadmap next.) See Custom fields → Deep links into external systems.
Find that campaign you can’t quite find
Section titled “Find that campaign you can’t quite find”Two complementary upgrades:
- The Campaigns list (
/dashboard/campaigns) now has a search box above the table. Type a partial name (or a product name) and the list narrows as you type — debounced 250 ms, server-side, so even a project with hundreds of campaigns finds the match. Pagination resets to page 1 with the new query so you don’t have to click through; clear the box to get the full list back. - The Cmd+K palette (and the matching search field in the top bar) now also reaches archived rows. The whole point of the palette is “I know this exists, just take me there” — hiding archived accounts and campaigns defeated that. Each result row now tags itself with
Archivedin the sublabel so you know what you’re opening.
If “calif” wasn’t finding Enacast California before — both surfaces now will.
Find duplicates straight from the campaign scoring page
Section titled “Find duplicates straight from the campaign scoring page”You no longer have to bounce out to Accounts → Find duplicates when you spot two scored rows that look like the same business. The scoring review page (/dashboard/campaigns/{id}/scoring) now has a Find duplicates button in the header that runs the same scanner — but scoped to this campaign. It shows the count first (“found 3 duplicate groups in this campaign”), then lets you AI-review, bulk-merge ≥90%, or merge group-by-group exactly like the project-wide flow. Cross-campaign twins still appear in the same group so the merge keeps the full picture. See Merge duplicates.
Download a CSV of the campaign’s approved accounts
Section titled “Download a CSV of the campaign’s approved accounts”The campaign detail page now has a Download CSV button next to Add new account. By default it pulls the four columns most people want — Name, Website, Location (City, Country), and AI Score — and only the rows you’ve marked Approved. A modal lets you widen the row filter (Approved + Pending, or every status), and tick on/off any of ~40 optional columns grouped by topic: contact info, address parts, account socials, business profile, pipeline & tags, workflow / audit. Project-defined custom fields show up automatically in a dedicated section, so per-company schemas (e.g. “Annual revenue”, “Industry segment”) are one click away. The file streams as text/csv with a sensible filename like cold-outreach-q3-approved-accounts-2026-05-24.csv.
Pagination + search across long lists — past row 20 is now reachable
Section titled “Pagination + search across long lists — past row 20 is now reachable”Six dashboard surfaces used to silently truncate at 20 rows: Campaigns (/dashboard/campaigns), Projects (/dashboard/projects), Channels (/dashboard/channels), Tasks (/dashboard/tasks), and the campaign / channel expense panels. None of them showed pagination controls, so a company with 30 campaigns / 25 channels / 100 tasks looked correct but quietly dropped the bottom of every list. Each page now carries Next / Previous controls and the StatCards at the top read off /statistics/ endpoints so totals are right regardless of which page you’re on. The Tasks page also now searches server-side, so typing a name finds matches in any page. The Cmd+K / search bar got the same treatment — typing now searches every account, campaign, and product in the project on the server instead of client-filtering the first 20 of each. If you’ve been hunting for a row that “should be there but isn’t showing up”, this is why.
Cancel works during the “Preparing accounts” phase too
Section titled “Cancel works during the “Preparing accounts” phase too”The Cancel run button used to only respond once the worker had finished scraping every account website and started AI scoring batches — on a 500-account campaign that could be 5–10 minutes of unresponsive cancel. Now there are cancel checkpoints inside the website-scraping and AI-fill phases too: clicking Cancel halts the worker at the next safe boundary (one in-flight HTTP request, ~15s — or up to a ~3-minute Gemini call’s timeout if the worker was deep in an AI-fill call). Whatever content was already scraped lands in the cache, so retry skips that work and only does the unfinished part.
Stuck scoring runs now self-recover faster
Section titled “Stuck scoring runs now self-recover faster”The orphan-resume sweep gained a second, much faster detector: a run stuck “in progress” for 30 minutes with zero accounts processed is treated as wedged immediately, rather than waiting the full 2 hours. This catches the most common wedge shape (worker deadlocked inside the enrichment phase) 4x faster, so the Score N remaining button reappears for retry within an hour of clicking Score remaining on a doomed run. The 2-hour hard backstop stays as the fallback for slow-grind wedges that DO write progress but won’t ever finish.
Stuck scoring runs now self-recover within an hour
Section titled “Stuck scoring runs now self-recover within an hour”Previously, if a scoring worker crashed in a way that prevented its own error handler from running (hard time-limit SIGKILL, out-of-memory, or a deep network hang in the enrichment phase), the campaign would sit at “AI scoring in progress…” forever — and the only way out was the Django admin or a direct API call. There’s now an automatic safety-net sweep on the server: any scoring run stuck “in progress” for more than 2 hours is flipped to “failed” with an explanatory note, so the Score N remaining button reappears for retry. The sweep also runs immediately on every server restart, so deploys clear wedged runs without delay.
Live scoring progress banner on the campaigns list
Section titled “Live scoring progress banner on the campaigns list”After kicking off scoring (or batch-scoring) you no longer have to refresh the page or click into each campaign to see how far it’s got. The Campaigns list now shows a live banner at the top whenever any campaign in the active view is mid-scoring — aggregate processed / total accounts, pending count, ETA, and the names of the campaigns currently running. Each name links straight to its score-review page. The banner polls every 5 seconds and ticks the ETA every second so it never feels frozen. Disappears once everything is done.
Score remaining accounts on every campaign in one click
Section titled “Score remaining accounts on every campaign in one click”The Campaigns list page (/dashboard/campaigns) now carries a Score remaining (N) button in the top-right action bar — N is the number of active campaigns that have unscored accounts and a target persona set. Click it once and AI scoring fans out across every eligible campaign in your active company. Confirmation dialog up front, summary alert at the end listing exactly which campaigns dispatched, which were skipped, and why (“no target persona yet”, “all accounts already scored”, “already scoring”). Already-scored accounts aren’t re-scored or re-charged. Archived campaigns are never touched. See Can I score remaining accounts on all my campaigns at once?.
Cancel a stuck scoring run from the scoring page
Section titled “Cancel a stuck scoring run from the scoring page”The scoring review page now carries a Cancel run button in the header while a scoring run is in progress. Until today, if a run wedged (deploy interrupted mid-batch, model API stall, worker recycle) there was no way to clear the “AI scoring in progress…” banner from the UI — the other action buttons stay hidden to prevent double-charging, so a stuck run could only be unblocked from the Django admin. Now you click Cancel run, confirm, and the Score N remaining button reappears immediately for retry. Already-scored accounts aren’t re-charged on the follow-up retry. Same action is also available via POST /api/campaigns/{id}/cancel-scoring/ for scripted recovery — idempotent, tenant-scoped. See My scoring run looks stuck — how do I unwedge it?.
Cancel is cooperative: the worker finishes the current 10-account batch before stopping (~10–30 seconds), so the scores already in flight land in the cache and aren’t thrown away. If the worker is mid-website-enrichment when you click Cancel, it’ll stop after enrichment completes — which can take a couple of minutes on a large worklist.
Internal notes per row on the outreach inbox
Section titled “Internal notes per row on the outreach inbox”Each row in Campaigns → … → Outreach now carries a free-text Internal note field. Use it to track per-lead prep context that doesn’t fit into status / tags: “demo live since Monday — wait a few days before pinging”, “warm intro from Ana coming Thursday”, “called once, no answer, try after 4pm”. Notes show as a sticky-note snippet on the row preview so you can scan the queue and spot which leads have context attached, and edit them inline at the top of the conversation pane (saves on blur). The field is per-campaign-account — the same lead in two campaigns gets two independent notes. See Internal notes per campaign-account.
”Override score” button on the scoring review page actually works
Section titled “”Override score” button on the scoring review page actually works”The Override score link on each prospect card in Campaigns → … → Score review opens a dialog where you can set a manual 0–10 score that supersedes the AI score for this campaign only. Sibling campaigns sharing the same product and goal aren’t affected — the AI score they read is untouched. A Clear override button on the dialog reverts back to the AI score. Before today the link logged to the browser console and did nothing; now it does what it always implied. See override_score on CampaignAccount.
Filters on the outreach inbox
Section titled “Filters on the outreach inbox”The campaign-wide outreach page (Campaigns → … → Outreach) now carries a Filters panel below the search box. Narrow the list by review status (approved / pending / rejected), match label (excellent / moderate / mismatch), score range (min/max on the 0–10 AI score), or channel (Instagram, LinkedIn, email, X, WhatsApp, phone, SMS, other — keeps only accounts you’ve touched on any of the picked channels). Pick any combination — they all AND together. The tab badges stay pinned to the true campaign size; only the visible list shrinks. The panel opens with Approved + Excellent pre-selected so you land on the work-this-now queue; clear those pills to widen the view. See Inbox filters.
Scoring progress bar actually moves now
Section titled “Scoring progress bar actually moves now”The in-progress bar on the scoring review page used to jump straight to 100% the moment you kicked off a rescore — especially the “Give feedback & rescore” flow, where every account already had a cached score and the bar had nothing fresh to count. The bar now tracks this run’s progress: 0 at dispatch, ticks up as each batch lands, lands at 100% when the run finishes. ETA recalculates against the same per-run number, so it stops claiming “less than a minute” the entire time. No action needed — just refresh the scoring page.
Exclude a campaign from global stats
Section titled “Exclude a campaign from global stats”The campaign detail page has a new Aggregate stats row with an Exclude this campaign from global stats checkbox. Tick it for experiments, calibration runs, or one-off imports of pre-existing customer relationships — their outreach and replies stop counting toward the Dashboard funnel, cohort rates, the High-ICP reached tile, and the by-product / by-channel roll-ups on Stats by product and campaign. The campaign’s own detail page still shows real numbers; its row on the breakdown table is tagged with an amber Excluded chip so you can spot it. See Exclude from global stats.
”Give feedback & rescore” on the scoring review page
Section titled “”Give feedback & rescore” on the scoring review page”You can now retune scoring without leaving the review queue. The scoring page has a new Give feedback & rescore action that opens a modal with the campaign’s scoring brief — revise the text, hit save, and every account in the campaign is re-evaluated under the new brief. Approvals, rejections, and per-account overrides are preserved; only the AI score and reasons change.
If you’ve ever scrolled the queue and thought “the AI is missing X” — that’s the workflow this enables. Type “accounts must have a physical retail location” or “we’re scoring chains too high”, save, and the next time you scroll the queue every row reflects that guidance.
The modal warns you if other active campaigns share the same product and goal — scores are cached per (product, goal), so a rescore on this campaign overwrites what those siblings show too. The scoring concept page explains the caveat and the workarounds.
Also rewritten: the What the model sees section now lists every input (21 items) in prompt order with size caps, so you can predict exactly what the AI is reading for any given account.
Per-campaign scoring brief
Section titled “Per-campaign scoring brief”Campaigns now carry an optional Scoring brief — a free-text overlay that’s appended to the scoring prompt below the Product value proposition. Use it to tilt the rubric for one campaign without regenerating the target persona: “down-weight chains; prioritise owner-operated stores in cold-climate cities”, “weight community-event capability heavily”, “this is a partnership push targeting accounts that already sell adjacent gear, not end-customers”. Edit it inline on the campaign detail page; the next rescore picks it up.
The brief is framed as “applies on top of the persona above” — it shifts emphasis, it doesn’t replace the persona. If you find yourself wanting to override the persona entirely, the right move is still to revise or regenerate the target persona.
Editing a Product’s value proposition now feeds scoring directly
Section titled “Editing a Product’s value proposition now feeds scoring directly”Until today the Product description only influenced scoring through the target persona — meaning a sharpened value-prop didn’t reach the scorer until you regenerated the persona. The scoring prompt now carries the Product’s name and description on every score, so an edit to the description shifts results on the next rescore without a persona regen. Example URLs remain the single biggest calibration lever; the description sits below them.
Imports no longer stall on rare unsafe characters
Section titled “Imports no longer stall on rare unsafe characters”Enrichment runs against websites that occasionally serve text with characters the database can’t accept verbatim — typically binary content slipped past the page-type check, or odd unicode escapes returned by the AI extraction step. A single tainted record used to fail the whole batch, the import would retry on the same bytes every minute, and progress would sit pinned near the top end of the bar until the run eventually gave up.
The enrichment pipeline now strips those characters before saving so the rest of the batch lands cleanly. No action needed on your side. If you had an import wedged on the old behavior, hit Retry on the Tasks page and it’ll resume from where it stopped.
Channels, customer value, and ROI per channel
Section titled “Channels, customer value, and ROI per channel”Inbound now has its own home. Acquisition channels (Adwords, Meta, LinkedIn Ads, organic search, organic social, referral, event, partner, cold inbound) are first-class objects with their own monthly spend, funnel counts, and customer-value rollups — separate from Campaigns, which stay focused on outbound efforts. Each channel exposes a Cost summary (per-currency totals, primary currency, CAC per acquired / responded / closed), a Value summary (captured lifetime value across every customer attributed to the channel, grouped by source), and a ROI ratio that drops the math when currencies mismatch instead of producing misleading numbers. See Acquisition channels.
Lifetime value tracking landed in the same change. Set a Default account value on a Product, override at the Campaign level for higher- or lower-value segments, or enter the actual deal value on individual Accounts at close. The chain auto-snapshots when status changes to customer and operator-entered values are sticky — future product price changes never retroactively rewrite historical customer values. The captured value powers the per-channel and per-campaign ROI rollups, plus a new by-channel column on the dashboard breakdown. See Account value and the how-to guide.
Ad spend that used to live on Campaigns (adwords, meta_ads, linkedin_ads, other_ads kinds) was migrated automatically to the channel surface during the rollout — every historical row is preserved, just attached to a Channel instead of a Campaign. Campaign expenses now only carry agency / labor / software / content / event / other.
Conversation messages no longer require a Campaign membership. An inbound lead from Adwords that hasn’t been enrolled in any outbound campaign can still have their email reply logged on the Account directly — the message scopes to the Account and the campaign attribution is optional. Existing logged messages were backfilled automatically.
Tasks page now shows per-run AI cost and token usage
Section titled “Tasks page now shows per-run AI cost and token usage”The Tasks page now carries a Cost column next to “Started At” — every row shows the USD spend on AI tokens and external API calls (Google Maps Places, Tavily) for that run, plus the total tokens used:
$0.0234 · 12.4k tokHover the cell for a per-model breakdown — useful when you want to know whether a costly run was the cheap Gemini lite model running a lot of cheap calls, or a single batch that swapped in a more expensive model. Two-line tooltip shape:
Total: $0.0234 · 18 callsTokens: 11.2k in / 1.2k out
google-gla:gemini-3.1-flash-lite: $0.0180 · 12 calls · 11.2k tokgoogle_maps: $0.0054 · 6 callsThe number is actual spend, not a forecast — it grows live while a run is in progress and lands at its final value when the run completes. Sort the Tasks page by Cost desc when auditing AI spend, or after a long enrich batch to spot the one outlier that cost ten times the rest. Past runs that pre-date AI cost tracking show — (we don’t have data for them); every new run is tracked.
This is the same per-model rollup that already lived on the Django admin Research detail page; now it’s surfaced on the operator-facing Tasks page so you don’t need admin access to see it.
Download the docs as an offline manual — EPUB, Kindle-sized PDF, or A4 PDF
Section titled “Download the docs as an offline manual — EPUB, Kindle-sized PDF, or A4 PDF”These docs now ship as a downloadable manual in three formats, all generated from the same source so the content stays in sync. Grab the one that fits your reading setup:
- EPUB (~140 KB) — the right answer for any e-reader. Reflowable text scales to the screen, so it’s readable on a Kindle, Kobo, Boox, Apple Books, or your phone’s reading app. Kindle users: Amazon’s Send to Kindle (email, web uploader, mobile share, or browser extension) accepts the EPUB directly and converts it on their end. No DRM.
- Pocket-edition PDF (~2.6 MB, 5.5”×8”) — fixed-layout PDF in mass-market paperback dimensions, sized for 6-7” e-ink readers (Kindle, Kobo, Boox) so text is readable without zooming. Use this if you prefer PDFs over EPUBs or your reader doesn’t reflow them well.
- PDF for A4 print or laptop reading (~2.5 MB, A4) — the standard letter-paper variant. Best for printing a section to mark up, or reading on a tablet / laptop.
Each format has a cover page, table of contents, and every concept / how-to page as a chapter. The PDFs are regenerated on every docs deploy; the EPUB is regenerated when a docs change ships. No login required.
Wrong-kind URLs are auto-rediscovered, and Research runs show what they spent
Section titled “Wrong-kind URLs are auto-rediscovered, and Research runs show what they spent”If an imported account’s website field turns out to be an audio stream, a PDF, a download link — anything that isn’t a webpage — enrichment now detects that on the first byte of the response, skips the 15-second timeout the old code suffered, and runs a rediscovery pass to find the company’s real homepage. Free hints come first: many streaming servers expose a homepage URL in their headers (Shoutcast/Icecast icy-url), and that hint wins when present. If there’s no hint, the website-discovery agent runs and proposes a candidate URL, which gets verified against the same first-byte check before the swap happens. The original URL is preserved in the account’s notes with a one-line audit explaining what it was (audio stream / PDF / …) — nothing is silently lost. Particularly useful for radio-station / podcast / streaming-service imports where the source CSV stamped the playback URL instead of the company website.
The Research admin detail page now carries an API cost rollup field showing every model that ran during the research, with per-model call count and dollar cost, plus a separate Enrich result field with a per-run audit table — input accounts, sites scraped, fields filled, LLM-fallback fills, URLs rediscovered, duration. Same dashboard you already get for campaigns under “Cost summary”, now scoped to one research run instead of one campaign. Most operators won’t notice the change; if you’re triaging “this run took an hour, what did it actually do?” this is where you look.
Enrich button now has three modes — and the Tasks page shows real progress
Section titled “Enrich button now has three modes — and the Tasks page shows real progress”The Enrich button on the Accounts page (next to Import / Export / Find Duplicates) now opens a small dialog with three radio choices: Only non-enriched accounts (default — skips every account that already has scraped website content), First 50 (test run) (a cheap smoke test against the 50 oldest non-enriched accounts before you commit to a multi-hour project-wide pass), and Currently filtered accounts (re-applies the page’s filter set — search, status, country, business type, the advanced filter DSL — and enriches exactly those rows). The “currently filtered” mode is what you reach for when you want to enrich every prospect in Spain before launching a campaign, or every account with website is empty so the discovery agent has a chance to find one.
The Tasks page now renders a real progress percentage instead of parking every in-flight run at 50% until it finishes. Tasks update every few seconds with rows-processed / rows-total so a long enrich actually looks like it’s making progress.
Large enrich runs are now safe across deploys
Section titled “Large enrich runs are now safe across deploys”Enrichment runs that take longer than a worker’s lifetime — a 4,000-account batch can easily take an hour or more on a slow scrape day — used to risk losing in-flight work if LeadHunter redeployed mid-run. The current pipeline slices large jobs into chunks small enough to each finish within the worker’s time limit, persists the chunk plan, and auto-resumes any chunk that gets interrupted. A redeploy in the middle of an enrich no longer parks the task at “in progress” forever; the new worker picks up where the old one left off automatically. An orphan-sweep also runs hourly as a steady safety net, so even silent worker deaths get cleaned up without operator action.
Behind the scenes the broker (Redis) now persists every queued task to disk and the worker only acknowledges a task once it actually finishes — so a SIGTERM mid-task re-queues for the next worker instead of being silently dropped.
Accounts now carry socials, year founded, and a headcount bucket
Section titled “Accounts now carry socials, year founded, and a headcount bucket”Three structured fields landed on every account — beyond the free-form business_type you already had — to make segmentation and outreach cleaner. year_founded is a numeric year you can filter on (“companies established 10+ years”); employee_count_estimate is one of five buckets (1-10, 11-50, 51-200, 201-1000, 1000+) and feeds into the scoring rubric so a 5-person shop and a 5,000-person enterprise are scored against the right shape; and six per-platform social URLs (linkedin_url, instagram_url, x_url, facebook_url, youtube_url, tiktok_url) replace the old free-text “Social:” line in the notes field. The account detail page now renders the socials as clickable chips, and the top stat row shows Founded + Employees alongside Business Type. CSV imports recognise common header variants — LinkedIn, IG, Twitter, Founded, Employees, Company Size — so a re-import from another CRM doesn’t need user-side column renames. Auto-enrichment fills all seven when the website states the value; manual edits are preserved on re-enrich. See Account → Company profile.
Big imports now survive a single bad row, and you can undo them in one click
Section titled “Big imports now survive a single bad row, and you can undo them in one click”Large CSV / XLSX imports (anything above ~2,000 rows) now run in chunks of 2,500 rows, each in its own database transaction. Before, a single rejected row in a 50k-row file would roll back every row that had already been imported — you’d start over from scratch. Now, only the chunk containing the bad row rolls back; everything that landed before it stays. The result panel tells you exactly which chunks committed and which didn’t, with the row numbers of the offending lines so you can fix them in the source.
Numeric fields are now also clamped pre-import instead of crashing the chunk. The previous behavior: if the AI column-mapper picked a “Votes” or “Bitrate” column as your rating field (because both are integers), every row whose value exceeded the rating’s 0–9.9 range hit a database overflow and rolled the chunk back. Now those values just become blank in the imported row and land in the same amber truncation summary that already covered over-long names and URLs (rating: 41,086 · language: 126 · name: 17). The import succeeds; you decide whether to fix the mapping and re-import, or to leave the misMapped column blank.
We also added an “Undo this import” button on the import result. One click, one confirmation, and every account that landed from the wrong file is gone — together with any campaign memberships, conversation messages, and scores that got attached to those rows. The import history record stays as an audit trail; only the account rows are wiped. 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 entered manually or imported from a different upload. Destructive and cannot be reversed — most operators want this right after an oops; after you’ve started using the data, fixing duplicates one-by-one in Accounts → Duplicates is usually safer.
Dashboard now says “Target persona” everywhere
Section titled “Dashboard now says “Target persona” everywhere”The shift from “Ideal Customer Profile” → “Target persona” started in the public docs and the Django admin a few releases back; the dashboard UI has now caught up. Page titles, form labels, error messages, empty states, and the workflow stepper all use the goal-neutral “Target persona” wording — sales prospecting still works the way you remember, but a press, recruiting, or investor campaign no longer has a sales-shaped label staring back at you. The buttons read “Generate target persona with AI” / “Regenerate target persona” / “Define persona”; the campaign detail page heading reads “Target persona”; the new-campaign wizard step reads “Define persona”. The score-review page header reads “Score review” instead of “ICP Model Validation”.
The change is copy-only — every URL, API endpoint, and saved-filter field still uses the same icp_* names. The stable “High-ICP Reached” StatCard label is unchanged so the metric stays recognisable next to the older docs and reports.
Goal-change controls + audit trail on the campaign detail page
Section titled “Goal-change controls + audit trail on the campaign detail page”The campaign goal is now a click target, not just a label. On the campaign detail page, the goal chip beside the campaign name opens a Change campaign goal modal: pick from the eleven goals (the current one is flagged), and a short reason is required so the audit trail explains why the change happened. The picker shows the goal’s one-line hint inline so a switch from sales → press is grounded in what each goal actually changes (ICP framing, scoring rubric, outreach intent, guardrails).
Every change lands in a new Goal history accordion that sits right above the stale-score banner — collapsed by default, mirrors the status-history pattern from the Account page. Each entry shows from → to as colored chips, the timestamp, the source (api / signal), and the reason in italics. Useful when months later you wonder “why does this campaign show stale scores?” — a goal flip is usually the answer, and now it has a paper trail.
Outreach by goal — breakdown table on the campaigns list
Section titled “Outreach by goal — breakdown table on the campaigns list”The Outreach by goal table now sits below the top-of-page stat cards on the campaigns list. One row per goal you’ve actually run a campaign at, with campaigns / accounts / outreached / responses / response rate per goal, sorted by outreached count descending. Response rates ≥5% render in green, between 0 and 5% in amber, zero in muted. The breakdown stays hidden until you have something other than the default sales goal in flight (so it doesn’t add noise for single-goal users).
This complements the existing per-product / per-campaign breakdown on /dashboard/stats — the campaigns-list table answers “which goal is doing the work this quarter?” at a glance, while the per-campaign breakdown drills into individual campaign performance.
Default LLM moved to the GA Gemini 3.1 Flash Lite
Section titled “Default LLM moved to the GA Gemini 3.1 Flash Lite”The platform’s default model has shifted off the -preview channel onto the now-GA gemini-3.1-flash-lite. Same architecture, same per-token price, but on the stable production endpoint that won’t be retired without notice. Cost reports under Usage and costs will show the new model name on calls fired after the rollout. Two internal agents (Tavily-backed deep research, and the website-discovery fallback) that had been pinned to a hardcoded model name now also follow DEFAULT_LLM_MODEL, so a single env var changes every agent’s model in lockstep.
URL lookup now works on more bot-protected sites
Section titled “URL lookup now works on more bot-protected sites”The Look up a website flow had been quietly degrading on a slice of sites — mostly Spanish car-trade, real-estate, and travel directories — that ship aggressive bot-protection. The lookup would fall back to a plain HTTP fetcher behind the scenes and get bounced with a 403. Two fixes shipped together: the headless browser is now actually installed in the production image (it wasn’t, on the deploy that introduced this regression), and the plain-HTTP fallback now sends the full set of headers a real Chrome / Firefox / Safari sends — including HTTP/2, Brotli compression, and Client Hints — with a one-shot retry from a different browser family on the “forbidden” responses bot-protection vendors use to fingerprint cheap scrapers. Sites that previously refused us now load and extract correctly.
Contract end date input on the Edit Account form
Section titled “Contract end date input on the Edit Account form”The contract_end_at field — already wired into saved-filter DSL and CSV imports — now has a first-class form input. Edit any account and a new Contract end date field (datetime picker) appears at the bottom of the form. Setting it opens up renewal-flow filters like “customers whose contract_end_at is in the next 60 days” without needing an import or an API call. Helper copy under the field reminds you it drives renewal-goal campaigns.
UI controls for per-purpose DNC and per-campaign role override
Section titled “UI controls for per-purpose DNC and per-campaign role override”Two recently-shipped backend features (per-purpose opt-outs and per-campaign role overrides) previously needed an API call to drive. They now have first-class controls on the existing detail surfaces:
- Per-purpose opt-outs, on the Account detail page (right column, between Relationship Status and Relationship Type): current opt-outs render as colored chips for the goals the account opted out of (sales / press / recruiting / …) — click the X on any chip to opt back in. “Record an opt-out” opens an inline picker with goal + source (manual / inbound request / import) + reason; saving appends an audit-trail entry visible in the new accordion below the picker. The blanket
do_not_contactstatus above is unchanged — the new panel is the goal-scoped layer for the GDPR / CASL case where an account opted out of one purpose but is still happy to engage on another. - Per-campaign role override, on the CampaignAccount row (in the campaign’s accounts panel, beside the score and outreach chips): a compact chip shows the override when set (typed in the relationship-type’s color); when unset, a faint dashed
+ roleaffordance lets you set one. Clicking opens a popover with all 13 relationship types — pick one to tell this campaign’s outbound drafter to treat the account through that lens regardless of its other tags. Useful when one Account plays multiple roles across campaigns (a customer who’s also a podcast guest who’s also a research participant). “Use the account’s tags (no override)” clears it.
Goal-aware UX shipped — every campaign surface adapts to the goal you picked
Section titled “Goal-aware UX shipped — every campaign surface adapts to the goal you picked”The campaign goal you set when creating a campaign now reshapes a lot more than just the AI prompts:
- The new-campaign wizard reorders. Goal is the first field on step 1 (it’s the most defining decision — it changes ICP framing, scoring, message intent, guardrails). The page hero adapts: sales reads “Let’s find your next customer”, press reads “Let’s get your story covered”, recruiting reads “Let’s source your next hire”, win-back reads “Let’s win back what was lost”, and so on for all eleven goals. Step 2 (“Define ICP”) similarly drops the sales-only heading in favor of “Define your <goal-label> target”.
- Bulk-add explanations name the goal. When a bulk-add hits a guardrail, the response now includes a goal-aware reason per blocked bucket: a Sales campaign tells you “this Sales campaign soft-blocks press accounts; pass
include_press=trueor switch the goal to Press / Influencer”. A Press campaign would never produce that message because press accounts aren’t blocked there. The frontend shows the reason directly under each blocked count. - Stale-score banner. Switching a campaign’s goal mid-flight no longer auto-rescores existing accounts (the cached scores were rated against the previous rubric). The campaign detail page now shows an orange “X accounts have scores from a different goal” banner with a one-click Rescore link when this happens.
- Cost panel third card. The Costs & expenses panel gains a third CAC card whose label adapts per goal: “Cost per closed-won customer” for sales, “Cost per press reply” for press, “Cost per candidate engaged” for recruiting, “Cost per renewal” for renewal, etc. Same denominator pattern as cost-per-outreached / cost-per-responded; the close count routes per goal (status=customer for sales-shaped goals, first-inbound for press/investor/recruiting/event/research/influencer/partnership).
ICP is now per (Product, Goal) — sales and press personas are independent
Section titled “ICP is now per (Product, Goal) — sales and press personas are independent”Until now the Ideal Customer Profile (now also called Target persona) lived on the Product alone — one ICP per product. With goals, that broke down: a sales campaign’s persona (“decision-makers at SaaS companies 50-200 employees”) and a press campaign’s persona (“publications and journalists covering our space”) on the same product were forced to share one row.
ICPs are now keyed by (Product, Goal). A press campaign of Product X gets its own persona with its own approval state; a sales campaign of the same product gets its own. Two campaigns of the same product and same goal still share one persona — the original “approve once, score once” reuse preserved per goal. Existing single-goal projects see no change (existing personas backfilled to goal=sales).
We also softened the terminology: the model is internally still called ICP for code stability, but admin labels and docs lead with “Target persona” since the goal-aware concept isn’t strictly customer-shaped anymore. See the rewritten Target persona and scoring page.
Per-goal competitor admission — research / event / press can include competitor accounts
Section titled “Per-goal competitor admission — research / event / press can include competitor accounts”The competitor relationship tag used to be a hard block on every campaign. That was sales-shaped thinking: a trade publication that covers your competitors is still a valid press pitch target; a roundtable invite legitimately includes peers; market research routinely interviews competitor employees.
Now competitor is per-goal: hard-blocked in sales / partnership / investor / influencer / recruiting / customer-expansion / win-back / renewal; admitted in press / event / research. personal_network remains the only universal hard block (bulk-blasting warm contacts via a campaign is always wrong).
Account model: contract dates + per-campaign role overrides + contact role kind
Section titled “Account model: contract dates + per-campaign role overrides + contact role kind”Three small but useful Account-side additions:
contract_end_at— optional renewal-cycle date. Renewal-goal campaigns can build saved filters like “customers whose contract_end_at is in the next 60 days”. NULL for accounts not on a renewal cycle (most prospects, all press / candidates / investors). Added to the saved-filter DSL and CSV column-import mapping.role_in_campaign(on CampaignAccount) — optional per-campaign lens. The same Account can be a paying customer AND a press contact AND a research participant; different campaigns view the account through different lenses. When set, the outreach-draft prompt receives a “Role for this campaign: treat this account as<role>regardless of its other tags” directive so the generated message respects the lens.Contact.role_kind— tags how to interpret the existingseniority/department/is_decision_makerfields. The sameseniority='c_level'value reads as “C-Suite executive” for a buyer, “Editor-in-chief” for a journalist, and “Senior career level” for a candidate. Defaultbuyerpreserves all pre-existing contacts. Seven kinds: buyer / journalist / candidate / investor / partner / influencer / other.
Per-goal breakdown on /api/campaigns/statistics/
Section titled “Per-goal breakdown on /api/campaigns/statistics/”The campaign statistics endpoint now returns a by_goal field — same shape as the top-level totals (campaigns / leads / outreached / responses / response_rate) but sliced per goal. “We did 5,000 outreaches this month” mixes sales pitches with press pitches with candidate sourcing; the breakdown surfaces the right slice per goal so analytics make sense. Single GROUP BY query — no extra round-trips.
Goal-change audit trail
Section titled “Goal-change audit trail”Every time a campaign’s goal changes, a {from, to, at, by_user_id, source, reason} entry lands in the campaign’s new goal_history field — same shape as Account.status_history. The PATCH endpoint accepts an optional goal_change_reason body field that gets stamped into the entry. Useful for debugging “why does this campaign show stale scores?” — a goal flip is usually the answer.
Per-purpose do-not-contact — opt-outs scoped to the right campaign types
Section titled “Per-purpose do-not-contact — opt-outs scoped to the right campaign types”Until now, marking an account as “do not contact” was all-or-nothing: blocking sales contact also blocked press pitches, research interview asks, event invites, everything. That’s not how real consent works. A customer who unsubscribes from your sales emails may still be happy to participate in a product research interview. A journalist who asks to be removed from a PR list may still attend an industry event.
LeadHunter now supports per-purpose do-not-contact alongside the blanket flag:
status='do_not_contact'remains the blanket opt-out — covers every campaign goal. Use for the strict GDPR / unsubscribe-everything case.do_not_contact_purposesis a list of campaign goals the account opted out of. An account with['sales']is blocked from sales prospecting campaigns but admissible in press, research, event, partnership, recruiting, expansion, renewal — anything that isn’t sales.
Recording an opt-out is a single API call (POST /api/accounts/{id}/record-dnc/ with {purpose, action}) — it appends a full audit entry to the account’s new dnc_history field. Same paper-trail shape as status_history: who, when, why, where it came from (manual, inbound_request, import). The campaign-add guardrail blocks any campaign whose goal appears in the per-purpose list, in the same blocked_dnc bucket as the blanket case — both layers are sticky, neither is overridable.
Existing status='do_not_contact' accounts behave unchanged. The new field is additive: empty by default, opt in to the granular layer as your consent flows mature. See Account → Per-purpose opt-outs.
Dashboard funnel routes “closed” per campaign goal
Section titled “Dashboard funnel routes “closed” per campaign goal”Until today the dashboard’s closed line counted exactly one thing: an account transitioning to status=customer. That definition only fits sales-shaped goals (sales prospecting, customer expansion, win-back, renewal) — every other goal showed closed: 0 forever, even when the campaign was succeeding.
As of this release the funnel routes per goal:
- Sales, customer expansion, win-back, renewal still close on the customer transition — unchanged.
- Partnership, press, influencer, investor, recruiting, event, research close on the first inbound. For these goals “they replied” is the win — a journalist taking the pitch, a candidate saying yes, a fund booking a meeting.
So a press campaign with one pitch sent + one reply now shows closed=1. A sales campaign with the same shape still shows closed=0 until the account transitions to customer. The initiated and responded columns are unchanged.
The new routing applies to both the daily totals and the cohort views (where outcomes are attributed back to the initiation date). Per-product / per-campaign breakdowns on the Stats by product and campaign page also use the new resolver.
Partnership and customer-expansion currently route to the inbound-response proxy — the more precise signals (partner relationship-type acquired, customer usage expanded) aren’t tracked in audit-log form yet, so we use the next-best signal. See Outreach reasons and targets → Success semantics for the full table.
Goal-keyed scoring — sales and non-sales campaigns no longer poison each other
Section titled “Goal-keyed scoring — sales and non-sales campaigns no longer poison each other”Running a Sales campaign and a Press campaign on the same product? Until today, scoring an account in one would silently overwrite the other — they shared a single score row keyed by (product, account). That was correct under “every campaign is sales” and quietly wrong the minute you mixed goals: the score you saw in your Sales campaign could be a press-fit number labelled as buyer fit, and vice versa.
As of this release, scores are keyed by (product, account, goal). Two campaigns of the same product at the same goal still share one score row (the original “score once” reuse — no extra Gemini cost). Two campaigns at different goals get independent score rows: rescoring one no longer touches the other.
The review-feedback few-shot — your approvals and rejections that calibrate the model — is also goal-scoped now. A press approval no longer contaminates sales scoring, and vice versa. See ICP and scoring → Score caching for the full mechanics.
Migration is automatic: existing scores were backfilled to goal=sales (the only meaningful pre-rollout value). If you’re running multi-goal campaigns on one product and want each goal to have its own scores, just trigger a rescore on the non-sales campaigns — they’ll get their own rows under their goal.
Campaigns now have a goal — sales is no longer the only reason to outreach
Section titled “Campaigns now have a goal — sales is no longer the only reason to outreach”Every campaign now carries an explicit goal that captures why you’re reaching out: sales prospecting (the default), partnership / reseller, press / visibility, influencer, investor outreach, recruiting, customer expansion, win-back / reactivation, event invitation, research interviews, or renewal. Eleven goals in total, covering the reasons businesses actually do outbound.
Picking a non-default goal changes four things downstream:
- The ICP gets reframed. A press campaign’s ICP describes the kind of publications and journalists you want to reach; a recruiting campaign’s ICP describes the candidate profile; a partnership campaign’s ICP describes complementary companies. The structure is consistent so the review-and-revise flow is identical — only the content shifts.
- Fit scoring rates the right axis. A 9 in a Press campaign means “highly relevant outlet, will probably take the pitch”; a 9 in Partnership means “strong shared-audience partner”. The 0-10 scale and the positive/negative/neutral reasons stay consistent so you can sort and review the same way.
- The draft outbound message matches the goal. A press pitch leads with the newsworthy angle, not a sales hook. A partnership pitch proposes collaboration, not a sale. A recruiting note is role-specific and asks for a 15-minute intro. Same product, same account, different message because the goal differs.
- The bulk-add guardrails flip. A Press campaign admits press / influencer / analyst accounts by default — they’re the target. Customer expansion and Renewal campaigns admit customers; sales and win-back keep blocking them as today. Competitor and personal-network accounts stay hard-blocked in every goal, and
do_not_contactis always sticky.
Existing campaigns defaulted to sales on rollout, so nothing changes for in-flight work. Pick the goal when you create a new campaign — there’s a goal selector on the new-campaign form right under the name field, and a coloured chip on the campaign card and detail header showing which goal is active. Switch the goal mid-flight if the angle shifts; the change re-applies to subsequent bulk-adds (existing scores stay as last computed — rescore explicitly if you want fresh ones under the new framing).
See the dedicated Outreach reasons and targets page for the full per-goal table and a worked example of three campaigns on the same product with three different goals.
Re-imports of the same file now skip everything (and tell you about it)
Section titled “Re-imports of the same file now skip everything (and tell you about it)”The bulk-import wizard learned to recognise files it has already seen.
- Pick a file → the Source label now defaults to the filename (
radio_stations_radiobrowser.xlsxbecomesradio stations radiobrowser). Beats the old'import'default for provenance. Edit it to anything you prefer — once you type, we stop auto-defaulting. - Re-uploaded the exact same file? The mapping step shows a yellow banner up top: “You’ve already imported this exact file” — with the prior import’s date, who ran it, how many rows landed, and how many of those accounts still exist. Cancel if it was a mistake; proceed if it was on purpose (the dedupe stack will skip the duplicates anyway).
- Map the source’s stable id column to
imported_id(Station UUID, CRM Record ID, Customer ID — whatever uniquely identifies a row in the source dataset). Dedupe now has a dedicated Imported ID level at the top of the stack, just below Google Place ID. A re-import resolves here first — even when name + city are missing or different. This is the level that makes monthly CRM re-exports clean: 52,000 unchanged rows skipped in 30 seconds instead of creating 52,000 duplicates because the export didn’t include a City column. - Every imported row now remembers the file’s fingerprint (an MD5 of the bytes). The account detail page can answer “which upload did this row come from?”, and you can slice by the same source even when multiple imports share the same label.
If the file’s been imported and the source has a stable id, a re-import is now safely a no-op — useful for monthly directory refreshes and CRM resyncs. See Import accounts → Imported ID for the mapping details and Deduplication, in order for where this slots into the existing stack.
LLM cost per campaign and per account, broken down by model
Section titled “LLM cost per campaign and per account, broken down by model”The campaign Costs & expenses panel now shows a second card next to your user-entered expenses: LLM & API auto-cost. It’s a USD total of every tracked LLM / Maps / Tavily call the platform fired on the campaign’s behalf, with a per-model breakdown (top four models by spend, plus a “+N more” tail) and a call count.
Two reasons this matters:
- Where the money went, by model. A campaign that used Gemini Flash for scoring and GPT-4o for outreach drafting now shows both lines separately — you can see at a glance whether the GPT-4o spend was worth it. Sub-cent totals format with four decimals so $0.000456 doesn’t read as $0.00.
- Per-account enrichment cost. Opening an account now shows its own auto-tracked LLM cost on the detail payload — answering “how much did this one row cost to enrich?” (website discovery + AI fill-in + any single-lead rescores). Detail-only, so the accounts list endpoint stays fast.
CAC is still computed from the user-entered side only — the auto-tracked side is reported in USD next to it, not folded in (your manual expenses may not be in USD, and we don’t FX-convert). The two cards together answer the “what’s this campaign actually burning?” question. Full picture in Usage and costs.
Big account imports run in the background, with live progress
Section titled “Big account imports run in the background, with live progress”The Accounts → Import wizard used to wait for the server to finish before you got a confirmation, which meant a 50k-row file could time out mid-flight and the modal would lie about how many rows it actually saved.
Three changes fix that:
- Files above ~2,000 rows hand off to a background worker, so the request returns immediately. The modal stays open with a live progress bar that ticks every few hundred rows; you can close it and the import keeps running. The result panel appears automatically when it finishes (or you can revisit it via the Tasks page).
- Over-long values are truncated to fit the column instead of failing the whole row. Each Account field has a max length (Name 255, Website 500, Language 50, etc.). Previously, one row with a 200-character
Languagevalue would silently roll back the entire 50k import. Now the value is shortened, the row imports, and the result panel shows a per-field count of what got trimmed. - Database-rejected imports are loud, not silent. If the batch fails for a reason truncation can’t fix, the result panel turns red, says “Import failed — nothing saved”, and lists the actual error so you can fix the source file. Counts that would have landed (had it succeeded) are struck through so the number can’t be misread.
Plus: a new Enrich accounts after import checkbox on the mapping step queues each imported account for website discovery + scrape (logo, email, phone, social links, site language) once the rows land. It’s off by default — tick it for trusted lists where the post-import enrich is worth the Gemini cost; leave it off for big directories. The enrich shows up as its own job on the Tasks page. Full walkthrough in Import accounts.
High-ICP reached stat on the dashboard and per campaign
Section titled “High-ICP reached stat on the dashboard and per campaign”A new High-ICP Reached card now sits next to the outreach funnel on the home dashboard, and next to the campaign stats on the campaign detail page. It answers a question that was previously a manual cross-reference: of the accounts the AI flagged as excellent matches (fit score 8 or higher), how many have actually been worked?
- On the dashboard (whole company): denominator is every distinct account with at least one excellent score in any product; numerator is the subset whose lifecycle status has moved past prospect (so
contacted,in negotiation,customer, orlost). A low percentage means there’s untapped high-fit inventory sitting in prospect waiting to be reached out to. - On a campaign: same idea but campaign-scoped. Denominator is the excellent-match accounts in that campaign; numerator is the subset whose campaign outreach status is
sent,bounced, orresponded. Useful for noticing when a campaign has scored well but stalled before outreach.
Alongside each percentage we now show an ETA to 100% — a rough projection of how many days you’d need to reach every high-ICP account if you keep going at your last-30-day pace. Hover the ETA for the math: remaining ÷ (last-30d-newly-reached ÷ 30). If you reached 6 new excellent-match accounts in the last 30 days and have 60 left, that’s about a year at this pace. ETA shows — when the last-30-day count is zero (no progress = no projection); restart outreach to see a real number again. Use it as a kick-yourself dial: a fast-moving campaign needs days, a stalled one needs years.
Remove low-fit accounts from a campaign in one click
Section titled “Remove low-fit accounts from a campaign in one click”After a campaign scores its accounts, you’ll often want to clear the low-fit tail before working the list — there’s no point spending outreach time on accounts the model already flagged as a poor match. The Campaign accounts panel now has a Remove low-fit button next to the In this campaign count. Click it, confirm the preview count, done.
What it removes: accounts whose AI fit score landed in the mismatch band (score < 5.0). Anything moderate or excellent stays untouched. Unscored accounts are left alone — they haven’t been judged yet.
What it keeps by default (so you don’t lose work):
- Accounts you’ve manually approved — your judgment beats the model’s.
- Accounts where outreach has already started (sent, scheduled, responded, bounced) — removing them would orphan the conversation thread.
The preview dialog tells you exactly how many are kept on each axis before you commit. The removal hits this campaign only; the same account in a second campaign of the same product is untouched. Details in Campaign → Pruning the low-fit tail after scoring.
Country picker on the filter builder
Section titled “Country picker on the filter builder”The Country filter on the accounts list is now a proper dropdown of country names instead of a free-text input. Pick from the list, no more typos or “Spain” vs “España” vs “ES” mismatches breaking the same filter across teammates. The previous free-text behaviour is retired; saved filters with old free-text country values keep working, but new filters use the picker.
Available operators tightened to match the picker: is / is not / is set / is empty. Comma-separated multi-country selection still works via the is any of operator in the filter DSL (use the DSL editor for that case).
Score distribution chart on campaign pages
Section titled “Score distribution chart on campaign pages”After a campaign finishes scoring, its detail page now shows a 10-bucket histogram of fit scores (0-1, 1-2, …, 9-10). It’s the fastest way to read whether the ICP is sharp — a healthy distribution leans bimodal (excellent + mismatch piles with a modest moderate middle); a flat distribution usually means the Product description or example URLs need work. See Campaign → Score distribution.
Archive and delete on accounts
Section titled “Archive and delete on accounts”Accounts can now be archived (with an optional free-text reason) or deleted from a new Danger Zone card at the bottom of the account detail page.
- Archive soft-hides the account from default lists while keeping every reference intact — campaign rows, scores, conversations, expenses, audit trail. Use it when the row still belongs in your history but you don’t want it cluttering daily views: the business closed, merged into a competitor, pivoted out of your market. The reason (“business closed Q1 2026”, “merged into X”) shows on the account header and in the audit trail. Unarchive is one click — no data loss.
- Delete is permanent — the row and every related campaign row, score, conversation, contact, and expense reference are removed. Requires typing the exact account name to confirm, since there is no undo.
Archive is the right answer for almost every “I want this out of my view” case; delete is for genuine mistakes (a test row, a typo, a manual data-entry slip the merge flow can’t fix). Details in Account → Archive and delete.
Dashboard funnel: tighter “Closed (won)” + deep-link to accounts
Section titled “Dashboard funnel: tighter “Closed (won)” + deep-link to accounts”Two related improvements to the outreach funnel on the dashboard home.
Click the funnel cards. The three card titles — Initiated, Responded, Closed (won) — are now links into the accounts list, filtered to the matching status (e.g. Closed (won) → customer + lost). So you can go straight from “1 won this month” to which one.
A win has to stick. Closed (won) used to count every to=customer entry in an account’s history, even when the operator immediately flipped the account back to prospect (a typical “oops, wrong account” pattern). Now the metric only counts transitions where the account’s current status is still customer or lost. Reverts no longer inflate the number.
Recording a pre-existing customer. When you change an account to Customer, you’ll see a checkbox: “Mark as pre-existing customer.” Tick it when the relationship already existed (you’re loading them from a spreadsheet, they were a renewal client before LeadHunter, etc.). The audit trail still records the change, but the dashboard’s Closed (won) count skips it — so importing your existing book of business doesn’t fake-inflate your win rate. Details in Account → Recording a pre-existing customer.
Track campaign costs, with cost-of-acquisition stats
Section titled “Track campaign costs, with cost-of-acquisition stats”Every campaign now has a Costs & expenses panel where you log everything you spent producing the campaign’s outcomes — Adwords spend, agency fees, internal labor hours, software subscriptions, content production, event costs. Each entry carries amount + currency + date + vendor + optional description. Ten kinds covered.
Three ways to add a line item:
- The inline form on the campaign detail page’s Costs & expenses panel.
- The 💸 quick-add button on every row of the campaigns list — opens a modal pre-filled for that campaign so you don’t have to open it first.
- (Programmatic ingestion via an automation tool is also possible if you need it; ask support.)
Existing entries are editable inline from the panel — click the pencil on any row, change the values, save in place.
LeadHunter then computes cost-of-acquisition (CAC) automatically:
- Cost per outreached account — total spend ÷ accounts reached out to.
- Cost per responded account — total spend ÷ accounts that replied.
- Cost per closed (won) account — visible on the Stats by product and campaign page in a new Cost column that shows total spend + the most meaningful CAC available for that row (closed → responded → initiated, in that order).
The campaigns list also has a Cost column showing each campaign’s running total in its primary currency.
Mixed currencies show per-currency totals without forcing a conversion — there’s no FX layer, so single-currency campaigns get full CAC and multi-currency ones surface a per-currency breakdown with an explanation. See Track campaign costs and CAC.
Track inbound leads from Adwords, Instagram, and referrals
Section titled “Track inbound leads from Adwords, Instagram, and referrals”Every account now carries an acquisition channel — the marketing channel that brought them in (outbound default, plus adwords, meta_ads, linkedin_ads, organic_search, organic_social, referral, event, partner, cold_inbound, other). LeadHunter was originally built around outbound; you can now log everything else with proper attribution.
Three places this shows up:
- Accounts → New account — a dropdown on the form, with a “starts at Contacted” hint when you pick an inbound channel. Inbound accounts skip the
prospectlifecycle stage because the lead has already reached out; the status change is recorded in the audit trail with the channel as the reason. - Account detail page — an Acquisition card on the right column for inline edits, plus a collapsible block showing the free-form acquisition metadata (UTM parameters from an Adwords click, an Instagram thread URL, a referrer’s email — anything useful for attribution).
- Accounts list — a new Channel column with the chip for inbound channels (outbound is hidden so the column stays quiet for the cold-prospecting majority). The filter builder picks the field up automatically.
See Track inbound leads for worked examples covering Google Ads forms, Instagram DMs, and referrals.
Outreach funnel on the dashboard
Section titled “Outreach funnel on the dashboard”The dashboard home now shows the three numbers that actually matter day-to-day: contacts initiated, contacts responded, and contacts closed (won) — each broken down for today, the last 7 days, and the last 30 days. Below that, a 12-month trend chart per metric with 7-day and 30-day moving averages, so you can see whether outreach effort and conversion are trending up or down even on noisy short windows.
Counts are by distinct account, not by individual message — three follow-ups to the same account count once as initiated. “Closed (won)” reads each account’s status history, so a deal that briefly closed-won six months ago still shows up in that month’s bucket even if the account later changed status.
The same numbers split per product and per campaign live on the Stats by product and campaign page — including (as of this release) the per-row cost column described above.
Scoring learns from your approvals and rejections
Section titled “Scoring learns from your approvals and rejections”The ICP Review page (Approve / Reject on each scored account) was always good for filtering — now it also calibrates future scoring. Once you’ve validated about ten accounts on a product, LeadHunter starts feeding your most recent approvals and rejections to the scoring model as concrete examples of “what good looks like here” and “what to skip even if the surface details look fine.” Below that threshold, the few-shot is held back — small samples are noisier than no sample, so the original example URLs stay the only anchor.
Same Account approved in two campaigns of the same product counts once; same Account rescored doesn’t get fed its own past verdict. The signal sticks to the product, so different products keep their own calibration.
Run the same product in different languages
Section titled “Run the same product in different languages”You can now set a communication language on each campaign that overrides the product’s default. Run one campaign of the same product in Spanish for the Spain market and another in English for the UK — without duplicating the product. The campaign’s language drives the AI-generated ICP summary, the score reasons attached to each account, and the outbound messages you draft.
If an individual account has its own language set, that always wins — LeadHunter will write to them in their language regardless of which campaign reaches them. See Campaign → Language for the full fallback chain.
Smarter campaign accounts panel
Section titled “Smarter campaign accounts panel”The campaign’s accounts view now shows in-campaign accounts side-by-side with the available pool. You can compare what’s already in the campaign against what’s still out there without losing context — handy when you’re filling out a campaign in passes rather than all at once.
A new single-account “Add new account” shortcut lets you slot in one specific company on the spot, without going through bulk-add or import.
Reach estimator on saved filters
Section titled “Reach estimator on saved filters”When you build a campaign from a saved filter, LeadHunter now tells you exactly how many accounts the filter will pull in before you commit. The estimator works against the real backend (not a placeholder) and respects all the outreach guardrails — so the number you see is the number that will actually land in the campaign.
Inline campaign renaming
Section titled “Inline campaign renaming”You can now rename a campaign directly from its detail page — one click, no extra modal. Useful when a campaign’s scope shifts mid-run and the original name no longer fits.
ICP “Pending approval” → “Approve” button
Section titled “ICP “Pending approval” → “Approve” button”When LeadHunter generates an ICP for your product, it sits in a Pending approval state. You’ll now see a clear Approve button on that state — review the draft, click approve, and the campaign moves on. Previously this was a small “Save” button buried in the form.
Bug fix: campaign rename no longer crashes the page
Section titled “Bug fix: campaign rename no longer crashes the page”A bug that broke the page for some users when renaming a campaign in production has been fixed.
Earlier in May 2026
Section titled “Earlier in May 2026”Lead is now Account
Section titled “Lead is now Account”Lead has been renamed to Account. Same row, broader meaning: one record per organisation, whether it’s a prospect, a customer, a partner, a press contact, a supplier, or your own personal network. Calling all of those “leads” never fit — and it bled into the UI (“Mark lead as customer” felt off when the lead had been a customer for two years).
You’ll see the new name in the dashboard sidebar and the URL (/dashboard/accounts/). The product itself is still called LeadHunter — that’s the verb (hunting leads, finding leads), not the entity. Full concept: Account.
Relationship types and outreach guardrails
Section titled “Relationship types and outreach guardrails”Accounts now carry relationship types — a multi-select describing what each account is to you (e.g. customer, partner, press, supplier, competitor, candidate). One account can be more than one thing at once.
When you bulk-add to a campaign, LeadHunter now blocks competitors and your personal network automatically (never overridable), and soft-blocks suppliers, investors, press, analysts, candidates with a per-type “include them anyway” toggle. The dashboard tells you exactly which accounts were blocked and why.
Full list of types and the guardrail matrix: Account → Relationship types and Campaign → Guardrails.
ICP moved to Product
Section titled “ICP moved to Product”The Ideal Customer Profile now lives on the Product instead of on the Campaign. That means two campaigns of the same product share one ICP and one fit score per account — adding an account to a second campaign of the same product doesn’t trigger a re-score.
Need a fundamentally different audience for the same product? Create a second product with its own ICP. See Company and Product.
Real campaign outreach inbox
Section titled “Real campaign outreach inbox”The campaign view’s outreach tab is now powered by real data (it used to be a placeholder). You can see every conversation thread in a campaign at a glance, sorted by what needs your attention.
Archive vs delete on campaigns
Section titled “Archive vs delete on campaigns”Archive soft-hides a campaign while keeping every conversation, score, and account link intact — for when a campaign is “done” but you want the history. Delete is now name-confirmed: you have to type the exact campaign name to delete, which cascades to the campaign’s accounts and conversations. No more accidental deletion.
April 2026
Section titled “April 2026”Custom fields and saved filters
Section titled “Custom fields and saved filters”Define per-company custom fields for whatever your business tracks — license tier, contract renewal date, fleet size, certifications, anything. Combine them with standard fields in saved filters to query your account database however you need (e.g. “customers in Spain whose contract renews this quarter and whose tier is Gold”).
Filters can be private (yours) or shared with your team. Use a saved filter to bulk-add a slice of your database to a campaign in one click. See Custom fields and Build a saved filter.
Global Company switcher
Section titled “Global Company switcher”The Company switcher in the dashboard top bar is now persistent across pages — your active company stays selected as you navigate, and the choice survives a browser restart.
Manual communication log + auto-translate
Section titled “Manual communication log + auto-translate”LeadHunter became a communication log — paste outbound and inbound messages from your normal channels (email, IG, LinkedIn, WhatsApp, phone) and the AI keeps the per-account history straight, with auto-translation and AI drafting in five modes. See Messaging and Log a conversation.
Relationship status lifecycle
Section titled “Relationship status lifecycle”Every account carries a single lifecycle status (prospect, contacted, in_negotiation, customer, lost, do_not_contact). Sending an outbound message auto-promotes a prospect to contacted; logging a customer event auto-promotes to customer. do_not_contact is sticky and survives merges so opt-outs stay opt-outs. See Account → Status.
On the roadmap
Section titled “On the roadmap”Known gaps that are referenced elsewhere in these docs as “coming soon”. Listed here so they’re visible in one place. No firm dates.
- Plan / quota enforcement — usage is tracked and budget alerts now warn the team at 80% / 100% of a monthly budget, but nothing blocks operations at a threshold yet. Until enforcement ships, the per-provider dashboards (Google Cloud, OpenAI, Anthropic, Tavily) remain the hard backstop.
When these ship they’ll move from this section up into the chronological list.
The notes above are the user-facing version of every change you’d feel using the product. The full engineering-level changelog lives in the repository for the curious.