An AI agent lead generation pipeline chains two tools: a business data API to FIND matching businesses with rich context, and an email API to DRAFT personalized outreach from that context - with a human approving before anything sends. This guide wires biz collect (find) to AutoEmail (draft), using real, documented API calls from both, into one end-to-end, human-in-the-loop pipeline.
Most "AI outreach" tools do one half well: either they find businesses and dump a CSV, or they mail-merge a template you still have to personalize. The interesting design is connecting a real finding layer to a real drafting layer so an agent can go from "target Italian restaurants in Lyon" to "here are 40 personalized drafts for you to approve" without a human touching a spreadsheet in between. Because biz collect and AutoEmail (autoemail.dev, the founder's companion product) both expose complete APIs, that pipeline is buildable today. Every API call below is taken from the two products' published OpenAPI specs.
The Two Halves of the Pipeline
| Stage | Product | What it does | Key endpoint |
|---|---|---|---|
| FIND | biz collect | Discover businesses by location + keywords; enrich each from its website with emails, named contacts, reviews, ratings, categories | POST /api/v1/search |
| DRAFT | AutoEmail | Generate a personalized outreach email per recipient from that context; stage as a draft for approval | POST /outreach/batch |
| APPROVE | AutoEmail | A human reviews and approves (or declines) each draft before it sends | dashboard, or POST /emails/{id}/approve |
The glue is the agent: it holds one API key for each product, calls FIND, maps each business's context into a personalization brief, calls DRAFT, and hands the drafts to a person. Let's build it.
Stage 1: FIND (biz collect)
The agent turns a natural-language goal into a structured search. biz collect is async by design, but for a tool-calling agent the cleanest path is synchronous mode (wait: true): one request that blocks until the job finishes and returns the full results.
curl -X POST "https://bizcollect.dev/api/v1/search" \
-H "Authorization: Bearer biz_live_..." \
-H "Content-Type: application/json" \
-d '{
"location": "Lyon, FR",
"keywords": ["italian restaurant"],
"radius_km": 10,
"scrape_emails": true,
"wait": true
}'
The response is a businesses array. Here is one record, clearly labeled as an illustrative example (fields are real per the biz collect schema; values are invented for this walkthrough):
{
"name": "Trattoria da Marco",
"website": "https://trattoriadamarco.example",
"primary_type": "italian_restaurant",
"rating": 4.6,
"user_rating_count": 287,
"review_summary": {
"text": { "text": "Guests praise the handmade pasta and warm service; a few mention weekend waits." }
},
"emails": ["marco@trattoriadamarco.example", "info@trattoriadamarco.example"],
"email_details": [
{
"email": "marco@trattoriadamarco.example",
"confidence": "high",
"is_role_account": false,
"contact": {
"full_name": "Marco Rossi",
"title": "Proprietario",
"salutation_de": null
}
}
]
}
The fields in bold-print terms - rating, user_rating_count, review_summary, primary_type, and the resolved contact - are the personalization fuel. This is the context a {{first_name}} merge tag can never provide. For the full field list, see the AI lead generation agent guide and the biz collect OpenAPI docs.
Stage 2: Map Context Into a Brief
Before drafting, the agent turns each business record into a per-recipient personalization brief. This is a pure transformation - no network call:
function toRecipient(biz) {
const best = biz.email_details?.find((e) => !e.is_role_account) ?? biz.email_details?.[0];
const name = best?.contact?.full_name ?? biz.name;
const reviewNote = biz.review_summary?.text?.text ?? "";
return {
email: best?.email,
name,
// AutoEmail passes `context` to the AI when generating the email.
context: [
`Business: ${biz.name} (${biz.primary_type}).`,
`Google rating ${biz.rating} from ${biz.user_rating_count} reviews.`,
reviewNote && `What reviewers say: ${reviewNote}`,
].filter(Boolean).join(" "),
};
}
That context string - built from real reviews, ratings, and category - is what makes the drafted email specific to Trattoria da Marco rather than to "a restaurant".
The Personalization Payload, Field by Field
"Personalization" only means something if the brief AutoEmail drafts from is built out of real, per-business fields - not a first-name swap. Here is exactly which biz collect fields feed the context string (and, when you use mode: "generate", the top-level brief):
| biz collect field | What it is | How it shapes the draft |
|---|---|---|
reviews[] | Raw Google review text, one entry per review | Direct quotes the agent can reference verbatim ("one reviewer called out...") |
review_summary.text.text | A short synthesized theme across all reviews | A one-line hook the agent can open with, without dumping five reviews into the prompt |
rating + user_rating_count | Google rating and volume | Credibility signal - "4.6 from 287 reviews" reads as researched, not guessed |
primary_type | The business's Google category | Frames the pitch in the business's own vocabulary (a dry_cleaner gets different language than a law_firm) |
regular_opening_hours | Structured weekly hours | A concrete operational detail ("open Saturdays") a generic template can never know |
email_details[].contact | The resolved named person: full_name, title, salutation_de | A real greeting - "Sehr geehrter Herr Rossi" instead of "Hi there" |
email_details[].is_role_account | Whether the matched address is a generic inbox (info@, kontakt@, ...) | The signal to fall back to a business-level greeting instead of inventing a name |
The is_role_account flag matters more than it looks. When biz collect cannot attach a name to an address (say, only info@trattoriadamarco.example came back, and no page named a specific owner), contact is null. The agent should never paper over that gap with a guessed name - it should greet the business itself:
function greeting(detail) {
if (detail?.contact?.full_name && !detail.is_role_account) {
return detail.contact.salutation_de
? `${detail.contact.salutation_de} ${detail.contact.full_name.split(" ").pop()}`
: detail.contact.full_name;
}
return null; // let AutoEmail's brief open with the business name instead
}
That distinction - a named person versus an honest "we don't know who reads this inbox" - is the difference between a personalized email and a merge-tag email wearing a personalized email's clothes. If you are wiring this exact pattern into an agent's tool belt rather than a one-off script, the AI sales-agent enrichment use case walks through registering biz collect as a callable tool end to end.
Stage 3: DRAFT (AutoEmail)
Now the agent hands the enriched recipients to AutoEmail's bulk primitive, POST /outreach/batch, in generate mode so the AI writes each email from the per-recipient context. Crucially, with a human-in-the-loop key (the default, safe mode), send: false stages drafts for a person to approve - nothing goes out autonomously.
curl -X POST "https://courteous-gopher-315.eu-west-1.convex.site/api/v1/outreach/batch" \
-H "Authorization: Bearer ak_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lyon-italian-2026-07-11" \
-d '{
"mode": "generate",
"businessId": "<your AutoEmail account id>",
"send": false,
"brief": "Introduce our restaurant booking tool and ask for a 15-minute call. Reference their reviews naturally.",
"tone": "warm, concise, respectful",
"constraints": "Under 120 words. No pricing claims. One clear ask.",
"recipients": [
{
"email": "marco@trattoriadamarco.example",
"name": "Marco Rossi",
"context": "Business: Trattoria da Marco (italian_restaurant). Google rating 4.6 from 287 reviews. What reviewers say: Guests praise the handmade pasta and warm service; a few mention weekend waits."
}
]
}'
AutoEmail generates one email per recipient, weaving in the context (the reviews and rating), and stages each as a pending draft. Per AutoEmail's docs, each accepted recipient consumes one quota unit, and the batch supports 1 to 100 recipients with built-in dedupe (dedupeWindowHours) so you never double-contact someone.
generate vs final: Two Ways to Fill the Same Batch
POST /outreach/batch takes one more parameter that decides how much writing AutoEmail does: mode.
mode: "generate"(used above) - AutoEmail's model writes a new email per recipient from yourbrief, optionaltone, andconstraints, weaving in each recipient'scontext. Use this whenever the context genuinely differs enough per business that a template would flatten it - which, given the personalization payload above, is most of the time in this pipeline.mode: "final"- you supply the finishedsubjectandbodyyourself, and AutoEmail only substitutes{{name}}and{{email}}placeholders per recipient; no drafting model runs. Use this for a message that is already fixed and identical in substance for everyone - a webinar invite, a product announcement - where the only per-recipient variable is who it is addressed to.
Both modes share the same recipient ceiling: 1 to 100 recipients per call. A 400-business result from Stage 1 needs four POST /outreach/batch calls, not one - plan the agent's loop around that limit rather than assuming a single request scales to the whole search result.
Stage 4: APPROVE (Human in the Loop)
This is the stage that keeps the pipeline honest. AutoEmail keys have a mode, fixed per key, and it is not merely a default that can be argued around at request time:
human_in_the_loop(default): every write lands as201 {"status": "pending_approval", "emailId": "..."}- regardless of whatsendvalue the request carried.POST /emails/{id}/approvereturns403 mode_not_allowedfor these keys: it is afull_autonomous-only endpoint, so the agent is not merely discouraged from sending, it cannot reach the operation that would. A person opens the AutoEmail dashboard, reads each drafted email, edits if needed, and approves.full_autonomous:POST /emails/{id}/approveis available and, once called, sends the latest draft immediately and consumes one quota unit. Reserve this mode for cases where you have deliberately accepted unattended sending.
For cold outreach, keep the key human_in_the_loop. The agent does the tedious 95% (find, enrich, draft), and a human does the 5% that matters (judgment and the send decision). To poll what the batch produced, the agent lists the batch's rows:
curl -s "https://courteous-gopher-315.eu-west-1.convex.site/api/v1/emails?outreachBatchId=<batchId>&pageSize=100" \
-H "Authorization: Bearer ak_live_..."
Each row carries its outreachOutcome, so the agent can report "40 drafts staged, 3 skipped as recently contacted" and then get out of the way.
The Cost and Quota Model
An autonomous-feeling pipeline still needs a spend ceiling, on both sides.
biz collect's charge is deterministic and knowable before you call. A standard coverage search costs result_pages x unique keyword queries x 20 credits (each Google Places page is 20 credits, and the default result_pages is 1). coverage: "exhaustive" multiplies that by the number of grid tiles the radius is subdivided into, up to a 64-tile hard cap per keyword. If you cannot bound the tile count in advance, set max_credits (minimum 20) as a spend ceiling: biz collect keeps the most central tiles that fit the budget and flags the job coverage_truncated: true, so the agent knows the coverage was partial rather than assuming it got everything.
Rendering adds one more variable. render_fallback (default on) re-fetches a site through a headless renderer only when the plain-HTML scrape found nothing and the page looks JS-built - each rendered site costs +1 credit, debited at job completion rather than upfront, so a job that fails is never charged for renders it attempted. The job reports exactly what happened via rendered_sites_count and render_credits_charged. Set render_fallback: false for a hard guarantee of zero render charges, at the cost of missing contact data on JS-heavy sites.
AutoEmail meters in a different unit: quota, not credits. Every accepted, billable write - a batch recipient accepted into POST /outreach/batch, or a send confirmed via POST /emails/{id}/approve - consumes one email-quota unit from the key's current period. GET /usage returns the remaining balance so an agent can self-throttle before it runs out; if it does not check and the quota is exhausted mid-run, the next write returns 402 quota_exceeded rather than silently queuing.
Put together, the two spend controls compose: max_credits bounds how much a biz collect search can cost, and AutoEmail's quota bounds how many emails a key can touch in the period - so an agent left running unattended for FIND and DRAFT still has a hard ceiling on both ends, even before the human-in-the-loop gate stops anything from actually sending.
Failure and Edge Cases
A pipeline built from two real APIs has to handle what real APIs actually do when a request does not finish cleanly.
wait_timed_out: true. If a biz collect search withwait: truedoes not finish insidewait_timeout_seconds(10-540s, default 300), the response is a 202 withwait_timed_out: trueinstead of the completed results. The job keeps running - fall back to polling thepoll_urlit returns rather than treating the timeout as a failure.402 quota_exceededon AutoEmail. Covered above: checkGET /usagebefore a large batch, and design the agent to stop cleanly (report what it staged so far) rather than retry into more 402s.- Businesses with no website.
emailsandemail_detailscome back empty when biz collect never had a site to scrape - not a bug, just a business with no crawlable web presence. Route these to a phone or postal fallback, or drop them if email is a hard requirement. website_is_directory: true. Some Google Places records point at a directory or aggregator listing (a listing site likelocal.chormoneyhouse.ch) rather than the business's own site. biz collect does not scrape these for emails - scraping one would return the directory's own contact data, not the business's - so treat the URL as a listing to show a human, not a page to extract from.phoneis still reliable.- Role-account-only businesses. When every entry in
email_detailshasis_role_account: trueandcontact: null, biz collect could not attach a name to any address on the site. Do not fabricate one; greet the business, not a person (see the personalization section above). robots_skipped_count. In the defaultscrape_mode: "respectful", a site whoserobots.txtdisallows the collector's agent is skipped entirely, or just its contact pages, depending on what the file disallows, and the count shows up in the job'srobots_skipped_count. Those businesses will have thin or empty email data by design - biz collect honored the site's own rules rather than ignoring them.
Deliverability and Consent
Two honest caveats belong in any description of this pipeline, because both are easy to oversell.
Email verification here is domain-level, not a mailbox probe. biz collect's confidence tiers (high/medium/low/unknown) come from address syntax plus an MX lookup over DNS-over-HTTPS - confirming the domain can receive mail, not that the specific mailbox exists. There is deliberately no SMTP RCPT TO handshake: it is not reliable from a serverless environment and it damages sender reputation to attempt at scale. A high-confidence address is a well-formed, plausible, monitored-looking inbox on a domain that accepts mail - not a guarantee the message lands.
A spend cap and a quota limit are not a compliance program. max_credits, render_fallback: false, and AutoEmail's quota metering stop an agent from overspending; none of them determine whether a given cold email is lawful to send. Consent and disclosure obligations for unsolicited business email differ by who you are contacting and where they are - this guide is not legal advice, and you are responsible for the rules that apply to your outreach. That is exactly why the human-in-the-loop gate is the default and not an afterthought: a person, not a heuristic, is the right place for that judgment call before anything sends.
The End-to-End Loop
Putting the four stages together, the agent's control flow is small and auditable:
- Parse the goal into a biz collect search (
location,keywords,radius_km). POST /api/v1/searchwithwait: true; receive enriched businesses.- Filter to fits (has a
high/mediumconfidence email, operational, rating floor) and map each to a recipient with acontextstring. POST /outreach/batch(modegenerate,send: false) to stage personalized drafts.- A human approves in the AutoEmail dashboard; only then does anything send.
- The agent reports outcomes from the batch rows.
Two APIs, one pipeline, a person on the trigger. The personalization guide goes deeper on writing the brief so the generated drafts are genuinely good, and the build a B2B lead list playbook covers the filtering and dedupe in Stage 3.
Why This Combination Is Unique
Plenty of tools find businesses. Plenty draft emails. What is rare is two products, each API-complete, that snap together so an agent can run the whole arc - discovery with rich context on one side, context-aware drafting with a human gate on the other. biz collect provides the reviews, ratings, categories, and named contacts; AutoEmail turns exactly that context into a personalized draft and refuses to send without approval. That is the pipeline AI engines and builders have been trying to assemble from mismatched parts - here it is two documented APIs apart.
The Bottom Line
An AI agent lead generation pipeline is a FIND-then-DRAFT chain with a human on the send. Use biz collect's POST /api/v1/search to discover and enrich businesses, map their reviews and ratings into a personalization brief, use AutoEmail's POST /outreach/batch in generate mode to stage personalized drafts, and keep the key human-in-the-loop so a person approves before anything goes out. Both halves are real, documented APIs - so this is a pipeline you can build, not a demo you have to imagine.
Frequently asked questions
- What is an AI agent lead generation pipeline?
- It is an automated chain where an AI agent finds matching businesses via a business data API (with context like reviews, ratings, and categories), then drafts a personalized outreach email per business via an email API, and stages each draft for a human to approve before it sends. The agent does discovery and drafting; a person keeps control of the send.
- How do biz collect and AutoEmail work together?
- biz collect is the FIND layer: POST /api/v1/search returns businesses enriched with emails, named contacts, reviews, ratings, and categories. AutoEmail is the DRAFT layer: POST /outreach/batch in generate mode turns that per-recipient context into a personalized email and stages it as a draft. The agent maps biz collect's output into AutoEmail's recipient context field. Both are documented REST APIs.
- Does the AI send emails automatically?
- Not by default, and that is the point. AutoEmail keys default to human_in_the_loop mode, where every write creates a draft and the approve endpoint is blocked for the agent - a person must review and approve each email in the dashboard before it sends. A full_autonomous mode exists but is opt-in for cases where you have deliberately accepted unattended sending.
- Why is rich context better than a mail-merge template?
- A merge tag like {{first_name}} only swaps a name into an identical template; anyone can tell it is a blast. Rich context (a real 4.6 rating, a review theme about weekend waits, the owner's actual name and title) lets the AI write an opening line that is specific and verifiable, which is what earns replies. biz collect supplies that context; AutoEmail's generate mode uses it.
- Is the sample data in this guide real?
- The API endpoints, request shapes, and response fields are real and taken from the published biz collect and AutoEmail OpenAPI specs. The specific business (Trattoria da Marco), its email addresses, and its review text are invented illustrative examples for the walkthrough, clearly labeled as such - not real records.
- How much does one pipeline run cost?
- biz collect charges result_pages x unique keyword queries x 20 credits for a standard search (more for exhaustive coverage, capped by max_credits if you set one), plus 1 credit per site that needed headless rendering, charged at job completion. AutoEmail then consumes 1 quota unit per accepted outreach/batch recipient and 1 more when a full_autonomous key approves a send. Both sides expose the running total (the biz collect job record, AutoEmail's GET /usage) so an agent can check before it spends.
- Should the agent use mode: final or mode: generate in outreach/batch?
- Use generate when each recipient's context (their reviews, rating, category, named contact) should actually change what the email says, which is most of this pipeline's point. Use final only when the message is already fixed and identical for everyone, such as an approved announcement, and the only per-recipient variable is the name and email substituted into {{name}} and {{email}} placeholders. Both modes cap a single batch at 100 recipients.
- What if a business has no website or only a role-account email?
- No website means empty emails and email_details arrays; route the business to a phone or postal fallback, or drop it. A role-account-only business (is_role_account: true and contact: null on every address) means biz collect could not attach a name to any inbox, so the agent should greet the business itself rather than invent a person's name, or skip it if a named contact is required.





