---
title: "Lead capture and the interest list"
url: /interfaces/leads
---

# Lead capture and the interest list

> Forms on the generated site, and a tool for agents, that turn a visitor into a contact: confirmed, enriched at its price, synced to your CRM, kept with consent.

Each form on the landing site, and the same form as an agent's tool, turns a visitor into a contact kept with its consent.

**A contact, from a form to a Customer.** A visitor asks.

1. Captured: a form or a tool. Or refused: bot or bounce.
2. Confirmed (one link): its address.
3. Enriched (at its price): company and size.
4. Synced (a Source): to your CRM.
5. Converted (it signs up): a Customer.

Each step is a state of the contact, written as an Event.

## A demo form
The sales tracker's demo form asks for three fields. Each confirmed contact is enriched, then written to the team's CRM.

```ts title="definition.ts"
import { define } from 'apis.dev'

/** Deals for each Customer, with a demo form that writes each confirmed visitor to the team's CRM. */
export default define({
  name: 'pipeline', domain: 'pipeline.apis.dev',
  sources: {
    firms: { api: 'companyfacts.apis.dev', cost: { 'Company.get': 0.002 } },
    crm:   { system: 'crm', scope: 'you', sync: 'write' },
  },
  Company: { name: 'string', domain: 'url!#', deals: '<-Deal.company' },
  Deal: {
    title: 'string', amount: 'decimal(12,2)', company: '->Company.deals',
    stage: { open: [], qualify: 'opened', win: 'qualified', lose: ['opened', 'qualified'] },
  },
  offer: {
    Free: { month: 0, seats: 1, includes: { 'Deal.open': 25 } },
    Team: { month: 49, seats: 5, includes: { 'Deal.open': 1000 } },
    'Deal.open': 0.001,
  },
  leads: {
    forms:   { demo: ['email', 'company', 'useCase'] },
    confirm: true,                                           // one link before anything else
    enrich:  { api: 'companyfacts.apis.dev', cost: 0.002 },  // a lookup per contact
    to:      'crm',
  },
})
```

Your team reads a contact with `Contact.list` on the [Operator plane](/trust/planes.md); the CRM receives the same record.

```http title="Response"
HTTP/1.1 200 OK
Content-Type: application/json
X-Cost: price=0, currency="USD"
X-Request-Id: req_8e2b47d0c3f91a65b7e4

{
  "id": "con_12",
  "email": "dana@acme.com",
  "form": "demo",
  "source": "forms",
  "state": "synced",
  "consent": { "broadcasts": true, "at": "2026-10-28T10:02:11Z",
               "notice": "sha256:6a1f0c33" },
  "answers": { "company": "Acme", "useCase": "Track enterprise deals" },
  "enriched": { "employees": 240, "sector": "Software", "cost": 0.002 },
  "customer": null
}
```

## Where a visitor leaves an address
A contact joins the [interest list](/lifecycle.md#before), beside every unmet call, pre-order and sign-up, in the order each arrived.

| A visitor | Meets | The contact's `source` |
| --- | --- | --- |
| On the landing site | Each form in `leads.forms`, at its own path, with the fields it names | `forms` |
| An agent for a person | The `joinInterestList` tool over MCP, with the same fields as input | `forms` |
| A caller whose call missed | The 402's or 404's offer to hear when it serves | `interest` |
| A buyer of a pre-order | Its Checkout | `preorders` |

Each form meets the [bar for generated pages](/interfaces/standards.md), and the edge challenges bots before it accepts one. A refused submission costs nothing and writes no contact.

## Enrichment, at its posted price
Enrichment reads another [API product](/every-product.md) as a [Source](/patterns/dataset-api.md#how-a-field-is-filled): one lookup, at its posted price, per contact.

That price is the whole cost, with no fee on top. A lookup that misses costs what the enriching product charges for a miss.

With `confirm` on, enrichment waits for the confirmed address. It never runs on a personal address, such as one at a webmail domain.

## Consent, and an erase that reaches the CRM
The form asks for [consent](/interfaces/channels.md) to broadcasts in its own unticked box, and keeps the notice version beside the answer.

A contact who never ticked it still gets the confirmation and the reply it asked for, and nothing else. An unsubscribe joins the [suppression](/interfaces/channels.md) list at once.

`Contact.erase` removes the contact from the product, the interest list and the CRM it synced to, and keeps only its suppression. Lead capture into a CRM, with enrichment, is a Team feature.