Concept · 8 min read
Anatomy of an enrichment.
One API call returns roughly forty-three person fields. Here's the exact response shape, where every value comes from, and the prompts behind the AI-synthesised ones — using a fictional persona, Maya Patel, that we use across our marketing examples.
The input
Either field works alone. Both together is the strongest hint set — email anchors the company match, LinkedIn URL skips the search step.
POST /v1/enrichments
x-api-key: YOUR_API_KEY
Content-Type: application/json
{
"type": "person",
"input": {
"email": "maya.patel@northwindtools.com",
"linkedInUrl": "https://www.linkedin.com/in/maya-patel-dg"
}
}The response
A flat field map under result, plus per-field source attribution and per-source raw responses (truncated here for readability — full schema in the API reference).
{
"id": "enr_01h9z3k4m5n6p7q8r9s0t1",
"status": "completed",
"entityType": "person",
"result": {
/* identity ─────────────────────────────────────────── */
"full_name": "Maya Patel",
"first_name": "Maya",
"last_name": "Patel",
"email": "maya.patel@northwindtools.com",
"email_status": "verified",
"email_confidence": 0.97,
"linkedin_url": "https://www.linkedin.com/in/maya-patel-dg",
"phone_number": null,
/* role ─────────────────────────────────────────────── */
"title": "VP of Demand Generation",
"normalized_title": "VP, Marketing — Demand Gen",
"seniority_level": "VP",
"management_level": "Senior leader (manages managers)",
"department": "Marketing",
"subdepartment": "Demand Generation",
"functions": ["paid", "lifecycle", "events", "rev-ops handoff"],
"tenure_years": 2.5,
"reports_to": "CMO",
"team_size": 7,
/* company ──────────────────────────────────────────── */
"company_name": "Northwind Tools",
"company_domain": "northwindtools.com",
"firm_name": "Northwind Tools, Inc.",
"company_linkedin_url":"https://www.linkedin.com/company/northwind-tools",
"office_location": "San Francisco, CA",
"city": "San Francisco",
"state_region": "California",
"country": "United States",
/* signals ──────────────────────────────────────────── */
"previous_firm": "HubSpot",
"education": "MBA, Kellogg (2018) · BSc Economics, UC Berkeley",
"skills": ["demand gen", "lifecycle", "RevOps", "MarTech selection"],
"tech_stack": ["HubSpot", "Apollo", "Clay", "Mutiny"],
"recent_activity": "Spoke at SaaStr 2025: 'Agent-led demand gen, end-to-end'",
/* synthesis (LLM) ──────────────────────────────────── */
"outreach_angle": "Lead with Northwind's recent ARR milestone and Maya's HubSpot pedigree. Connect to her recent SaaStr keynote on agent-led demand gen. Reference the open req for a Senior Lifecycle Marketer — she's actively expanding the team.",
"contextual_insights": "Demand-gen leader scaling Northwind from $4M to $28M ARR over thirty months. Came from HubSpot's PLG team. Operator profile — cares about pipeline-conversion math, allergic to vanity metrics.",
"priority_score": 0.92,
"confidence_score": 0.91,
/* metadata ─────────────────────────────────────────── */
"data_source": "abm.dev v1",
"last_enriched": "2026-05-09T14:22:11Z",
"last_verified_date": "2026-05-08"
},
"fieldAttribution": { /* every field above mapped to source, see below */ },
"sourceResults": [ /* per-source raw response, see below */ ]
}Where every field comes from
Five buckets — identity, role, company, signals, synthesis — with the source, the extraction method, and a confidence number on each. The synthesis bucket is the one most APIs hand-wave; we walk through the actual prompts below.
Identity
| Field | Source | How it's derived | Conf. |
|---|---|---|---|
| full_name | Profile header | 0.99 | |
| first_name / last_name | Header tokenisation | 0.99 | |
| Hunter.io | Email finder against `northwindtools.com` + name | 0.97 | |
| email_status | Hunter.io | MX + SMTP RCPT probe (catch-all rejected) | 0.97 |
| linkedin_url | Input · LinkedIn search fallback | Trusted as input; otherwise resolved by name + company match | 1.00 (input) |
| phone_number | — | Not found in any source. Returned as null, not made up.Hallucination guard. | — |
Role
| Field | Source | How it's derived | Conf. |
|---|---|---|---|
| title | Current position headline | 0.98 | |
| normalized_title | Synthesis | Maps free-form titles to a canonical taxonomy (function + level) | 0.92 |
| seniority_level | Synthesis | Title classification | 0.96 |
| management_level | Synthesis | Combines title + team_size + reports_to chain | 0.86 |
| department / subdepartment | Synthesis | Title + functions vector | 0.94 |
| tenure_years | Current position start date → today | 0.99 | |
| reports_to | Synthesis | Inferred from company size + dept structure (not from a graph lookup)Confidence stays low when we can't verify — this would be flagged for the writeback diff before HubSpot accepts it. | 0.71 |
Company
| Field | Source | How it's derived | Conf. |
|---|---|---|---|
| company_name | Current company on profile | 0.99 | |
| company_domain | Email · LinkedIn | Email-domain extraction first; LinkedIn website fallback | 0.99 |
| firm_name | Perplexity | Legal entity name pulled from public filings + Crunchbase | 0.93 |
| company_linkedin_url | Linked from the person's profile | 0.99 | |
| office_location · city · state · country | LinkedIn · Perplexity | Profile location — cross-checked against the company's HQ string | 0.95 |
Signals
| Field | Source | How it's derived | Conf. |
|---|---|---|---|
| previous_firm | Most recent prior position | 0.99 | |
| education | Education section | 0.96 | |
| skills | Top skills — capped at 4 to keep the field useful, not a wall | 0.88 | |
| tech_stack | Tavily · Perplexity | Web search across her conference talks, BuiltWith, public job postsConfidence reflects that tech-stack inference is partial — only what they've talked about publicly. | 0.78 |
| recent_activity | Tavily | Web search for `"Maya Patel" SaaStr` in last 12 months | 0.94 |
Synthesis (LLM)
| Field | Source | How it's derived | Conf. |
|---|---|---|---|
| outreach_angle | Synthesis (Claude) | Aggregates identity + role + signals + company news through a tuned prompt | 0.85 |
| contextual_insights | Synthesis (Claude) | Same upstream context, different prompt geared at persona description | 0.83 |
| priority_score | Synthesis | Weighted score: ICP fit + recent intent + reachability | 0.92 |
| confidence_score | Synthesis | Aggregate of source confidences, weighted by field importance | — |
The synthesis prompts
Two of the forty-three fields — outreach_angle and contextual_insights— are written by an LLM (Claude) on top of the structured signals from the other sources. Most enrichment APIs treat their prompts as a black box; ours don't. Here they are.
outreach_angle prompt
# Role
You write the opening pitch for B2B outbound. Your job is one or two sentences
that the rep can paste into LinkedIn or email — specific, recent, and not
clichéd.
# Inputs (provided by the pipeline, not by the user)
- The person's full_name, current title, current company, tenure, and the
three most recent activity signals (talks, posts, hires, funding).
- The company's last six months of public news (Perplexity).
- The user's standing system_prompt + ICP description (from
/v1/enrichment-config).
# Constraints
- Reference at least one specific, recent, verifiable fact.
- No empty flattery ("I love what you're doing at X").
- No assumptions about pain ("I bet you're struggling with…").
- If you can't ground the angle in a concrete fact, return null and surface
via outreach_angle_status: "insufficient_signal". Don't make one up.
# Output
A single string, 1–2 sentences, second-person, no emoji.contextual_insights prompt
# Role
You write the persona-level summary that gives the rep enough to know how to
talk to this person — operator vs strategist, what they care about, what
they're allergic to.
# Inputs
- The same context bundle as outreach_angle.
- Plus: their public writing (LinkedIn posts, conference talks, podcast
appearances) summarised by Perplexity.
# Constraints
- 2–3 sentences.
- Specific verbs, no jargon. "Cares about pipeline-conversion math" beats
"data-driven leader."
- Acknowledge gaps openly — if the public footprint is thin, say so rather
than pad.
# Output
A single string, 2–3 sentences. Third-person.Override per request
PUT /v1/enrichment-config for a permanent change, or per-call via options.instructions on the enrichment request for a one-off override (e.g. tighter ICP, different voice, different emphasis).What we don't do
If the cross-source check fails on a field, we return null and surface a quality_warnings entry. We never fill a gap with a plausible-sounding made-up value. The audit trail at GET /v1/enrichments/{id}/sources shows every provider that was queried, what they returned, and which values were reconciled against which.