URL: /code/data/people/enrich · Id: `page:/code/data/people/enrich`

# Contact.enrich

> Fill a Contact’s missing title, employer, seniority and location from outside sources, each value marked with where and when it was read.

`POST https://api.sb/{your-domain}/contacts/enrich`

Fill a [Contact](https://startupbuilder.dev/business/nouns/contact)’s missing title, employer, seniority and location from outside sources, each value marked with where and when it was read.

- **Price:** 20¢ per 100 contacts
- **Success rate:** 99.99% of 136,987 calls
- **Freshness:** Fresh for 90 days
- **Emits:** `Contact.enriched`
- **Capability:** [Enrichment](https://startupbuilder.dev/systems/sales/enrichment)

```text title="Run it · Ask"
Fill in what we know about Dana Whitfield before I write to her; spend up to 1¢.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Contact.enrich({ contact: 'contact_FdmRdAKcbV', maxCost: 0.01 })
```

```json title="Run it · Response"
{
  "contact": "contact_FdmRdAKcbV",
  "filled": [
    "title",
    "organization",
    "seniority",
    "location"
  ],
  "values": {
    "title": {
      "value": "Director of Property Management",
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "sampled"
    },
    "organization": {
      "value": "harborview-pm.com",
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "sampled"
    },
    "seniority": {
      "value": "director",
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "sampled"
    },
    "location": {
      "value": "Seattle, WA",
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "sampled"
    }
  },
  "cost": {
    "usd": 0,
    "maxUsd": 0.002,
    "units": 1,
    "misses": 0
  }
}
```

## Options and prices
| You get | Input that selects it | Price | Success rate |
| --- | --- | --- | --- |
| Title, employer, seniority and location | `{"fields":["title","organization","seniority","location"]}` | About 20¢ per 100 contacts | 99.99% of 136,987 calls |
| The full profile with past roles | `{"fields":["history"]}` | About 64¢ per 100 contacts | 99.98% of 200,979 calls |

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `contact` | `Contact` | Required | A saved Contact id. Also served at `/contacts/{contact}/enrich`. |
| `fields` | `('title' \| 'organization' \| 'seniority' \| 'location' \| 'profiles' \| 'history')[]` | `every empty field` | What to fill. A field you already hold is skipped and costs nothing. |
| `maxAge` | `string` | `'90d'` | A value read more recently than this is kept, at $0, instead of reading again. |
| `maxCost` | `number` | `0.05` | The most this Contact may spend, in USD. Over it, the call is refused before it spends, or stops early with what it has. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `contact` | `Contact` | Never null: the Contact the values were written onto. |
| `filled` | `string[]` | Never null: the fields this call filled; empty when no source knew the person, at $0. |
| `values` | `Record<string, { value: string \| number \| null; observedAt: string; method: Observed['method'] }>` | Never null: one entry per filled field, each with when and how it was read. |
| `values.organization.value` | [`Hostname`](/code/data/types#hostname) | No current employer on record. |
| `values.seniority.value` | `'owner' \| 'c-level' \| 'vp' \| 'director' \| 'manager' \| 'individual'` | Unknown. |
| `cost` | [`Cost`](/code/data/types#cost), in USD | Never null. In try mode `usd` is 0 and `maxUsd` is what the call would have cost. |

## Read the result
| Field | Value | What it means | What to do |
| --- | --- | --- | --- |
| `values.organization` | Not the company the Contact was found at | They changed jobs. The old address will bounce or reach whoever inherited the inbox. | Stop their Sequence and find them again at the new company; a new job is a reason to write. (the job-change rule for cold email) |
| `filled` | Empty from an email address alone | An email resolves a person less often than a profile URL: about 72% across every kind of identity, against 99.99% from a profile URL. | Save the profile URL `Prospect.find` returns and enrich from that. (measured across 394,379 routed calls) |
| `values.*.observedAt` | Older than 90 days | Titles drift: a quarter-old title is a guess about today. | Read it again before a first email that quotes the title. (our planning number) |

## Effects
**Action class**

None: no approval rule names it, so it runs within its seat’s Permissions and Limits

**Modes**

- `try`: Reads the values and returns them; nothing is written onto the Contact and nothing is billed.
- `send`: Returns a pending Action at once; its Event carries the rows when the read finishes.
- `do`: Waits for the read and returns the rows, billed per result.

**Idempotency**

The same `Idempotency-Key` with the same input replays the first answer at $0; with other input it returns 409.

**Saved or shared**

Writes each filled value onto the Contact with where and when it was read; a value you entered is never overwritten. Readings about people are never shared across startups.

**Emits**

`Contact.enriched` with `{ contact, filled, values }`

**Performed by**

[Prospector](https://startupbuilder.dev/agents/sales/prospector)

**Price**

About 20¢ per 100 contacts. Free: a Contact no source knows, fields you already hold. `maxCost` defaults to 5¢. Every Contact in a 300-person list: 300 × 0.2¢ a contact is about 60¢.

**Reliability**

The first route succeeds 99.99% of 136,987 calls. If it fails, a second route answers at 99.98% of 200,979 calls, and you pay for one.

**Freshness**

A reading younger than 90 days returns at $0. Readings about people are never shared across startups.

**Batch and pages**

Up to 1 Contact a call. It answers in one response.

**Rate**

[The shared rate rules](/code/data#rate): 10 calls a second per key, and an array input counts as one call.

## Refuses when
This call has no refusals of its own. Every data call can also meet [the shared data-call refusals](/code/refusals#data-calls): [`filter-unsupported`](/code/refusals#filter-unsupported), [`input-missing`](/code/refusals#input-missing), [`limit-exceeded`](/code/refusals#limit-exceeded), [`max-cost`](/code/refusals#max-cost), [`rate-limited`](/code/refusals#rate-limited), [`sources-busy`](/code/refusals#sources-busy), [`venue-unsupported`](/code/refusals#venue-unsupported).

## When it waits
Returns in one response; nothing waits.

## Example

**Ask**

```text
Fill in what we know about Dana Whitfield before I write to her; spend
up to 1¢.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/contacts/enrich?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"contact":"contact_FdmRdAKcbV","maxCost":0.01}'
```

The response is the one in the Run it window above.

**CLI**

```bash
npx startup-builder do Contact.enrich --try --data '{"contact":"contact_FdmRdAKcbV","maxCost":0.01}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Contact.enrich({ contact: 'contact_FdmRdAKcbV', maxCost: 0.01 })"
  }
}
```

**SDK**

```ts
import { $ } from 'startup-builder'

const result = await $.try.Contact.enrich({
  contact: 'contact_FdmRdAKcbV',
  maxCost: 0.01,
})
```

To retry, send the same `Idempotency-Key`: the retry returns the first answer for $0. A new key reads again.

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/data#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `maxCost: 0.0001` | Refuses with [`max-cost`](/code/refusals#max-cost) | Any priced call with a maxCost of 0.0001 is refused before it spends: the price is above the cap. |
| `rate-limited.test` | Refuses with [`rate-limited`](/code/refusals#rate-limited) | Any call naming this hostname answers 429 with Retry-After, so your retry logic runs. |
| `sources-busy.test` | Refuses with [`sources-busy`](/code/refusals#sources-busy) | Any call naming this hostname answers 503 as if every route were busy, and nothing is spent. |

Watch it on a schedule: a Monitor runs this call and records an Event only when the reading changes.

```ts
await $.Monitor.create({
  verb: 'Contact.enrich',
  input: { contact: 'contact_FdmRdAKcbV', fields: ['organization', 'title'] },
  change: 'job-change',
  every: '4w',
})
```

## Used by
- **Workflows:** [Champion moved](/capabilities/keep-and-grow/champion-moved) · [Inbound routing](/capabilities/convert-visitors/inbound-routing) · [List refresh](/capabilities/find-customers/list-refresh) · [Post-engager harvest](/capabilities/find-customers/post-engagers)
- **Agent tasks:** [Account brief](/capabilities/reach-out/account-brief) · [Answer every inbound](/capabilities/convert-visitors/answer-every-inbound)
- **Jobs:** [Find your first customers](/capabilities/find-customers) · [Keep and grow customers](/capabilities/keep-and-grow)

## Related
[`Prospect.count`](/code/data/people/count) · [`Prospect.find`](/code/data/people/find) · [`Prospect.verify`](/code/data/people/verify) · [`ContactPoint.findEmail`](/code/data/people/find-email) · [`ContactPoint.verifyEmail`](/code/data/people/verify-email) · [`ContactPoint.findPhone`](/code/data/people/find-phone)