Quotes & Leads
Turn listings into leads — request-a-quote from a listing or the global Get Quotes page, double opt-in, paid lead subscriptions via Stripe, and an admin Lead Suite.
Why this matters
A directory that only links out leaves money on the table. Get a Quote (RFQ) turns browsing into a warm lead: a visitor describes what they need and it lands with the business — or with the best few businesses for the job. Owners can then pay to receive a steady stream of matching leads.
Turn it on
Flag: quotes: true in config/features.config.ts (on by default; implied by the localBusiness vertical).
Migrations: 0008_add_quote_requests.sql (the RFQ tables) and 0010_add_lead_subscriptions.sql (paid lead subscriptions).
How requests reach businesses
Requests target either one listing or several:
- Single — a Request a quote button on a listing page reaches that one owner.
- Multi — from a category/city page, or the standalone
/get-quotespage, the request fans out to the top matching pros. Recipient matching reuses the catalogue's own category/city filter, so nothing niche is hardcoded.
/get-quotes is a front-door request page: the visitor picks a service, city and timeline and the request fans out — no need to visit a listing first. It's linked from the header when the flag is on.
Leads are real by default
Double opt-in is on out of the box (requireEmailVerification: true): the requester verifies an emailed code before the lead is delivered. Codes are stored only as a SHA-256 hash. A hidden honeypot field silently drops bots, and the submission rate-limit bucket caps abuse. Delivery happens in exactly one idempotent place (deliverQuote()), so a lead is never sent twice.
Config knobs
File: config/quotes.config.ts
export const quotesConfig = {
maxRecipients: 3, // fan-out cap ("up to N businesses")
requireEmailVerification: true, // double opt-in
collectPhone: false, // ask for a phone number
consentRequired: true, // consent checkbox + text
leadContactGating: 'open', // 'open' | 'paid'
honeypotField: 'company_website',
};Lead-contact gating (monetization)
open(default) — every owner sees the full requester contact.paid— free-plan owners see a masked lead plus an upgrade CTA; only plans with thereceive_lead_contactfeature (inconfig/plans.config.ts) see full contact.
Paid lead subscriptions
Owners can subscribe to receive every matching lead in a category/city through a recurring Stripe subscription (dynamic price_data, so there's no Stripe Price to create). Active subscribers are folded into resolveRecipients alongside the top-ranked listings. The subscribe box lives in the owner's leads inbox. See Payments.
Where leads show up
- Requester: tracks their requests in a dashboard Leads tab.
- Owner: an inbox of incoming leads (with the subscribe box).
- Admin: the Lead Suite dashboard — KPIs, approve/delete, top cities.
Owner lead emails carry a CAN-SPAM footer; set BUSINESS_POSTAL_ADDRESS in your env so it's complete — see Email & Notifications.
Related
- Local Business Directory
- Payments
- Plans Config — the
receive_lead_contactplan feature.