Reference

The API contract

One service layer sits behind four surfaces, so the validation rules, the credit behaviour and the error text on this page are the same whether a request arrives over REST, over MCP, from the chat agent or from a CSV run. Everything below is generated from the schemas the API actually parses with.

Four surfaces, one contract

SurfaceWhereWhat it is
REST API/api/v1Search and enrichment over HTTP with an API key. Small requests answer inline; larger ones return a job you poll. Retries are deduplicated only when you send an `Idempotency-Key` header. Cursor-paged job results, and an OpenAPI 3.1 document generated from the same schemas the routes validate with.
MCP server/api/mcpModel Context Protocol over Streamable HTTP, so an agent in Claude, ChatGPT, Cursor or Codex can run enrichments as tool calls. Authenticated with the same API key as the REST API.
Chat agent/app/chatAn in-product agent that plans a run from a plain-language brief, shows what it is about to spend, and returns a table you can export.
CSV upload/app/enrichUpload a list, map your columns to the fields you want back, see a cost preview before anything runs, and download the enriched file.

Agent setup for the MCP server — endpoint, tools, client configuration — is on the MCP page.

Field catalogue

These are all of them. The list is a closed enum: a request naming anything else is rejected at the edge with a message saying what is valid, rather than returning a row with a silently missing column.

FieldApplies toWhat it is
emailPersonWork email address. Only returned when you ask for it by name — it is not in the default field set, it is the one field that costs several times a person record, and it is billed per address actually found.
linkedin_urlPerson + companyLinkedIn profile URL for a person, or the LinkedIn company page for a company. Returned as the search result's own URL rather than guessed.
titlePersonCurrent job title, read from the person's work history.
seniorityPersonNormalised seniority band, derived from the title. One of: founder, c_level, vp, director, manager, senior, entry.
departmentPersonNormalised department, derived from the title. One of: engineering, product, design, sales, marketing, finance, operations, people, legal, support, data, it, executive, other.
locationPerson + companyLocation as published, free text — for example “San Francisco, California, United States”.
countryPerson + companyCountry, derived from the location string.
companyPerson + companyCurrent employer's name for a person; the company's own name on a company row.
company_domainPerson + companyRegistrable domain of the employer for a person; the company's own domain on a company row.
headlinePerson + companyProfile headline for a person; the company's short description on a company row.
phonePersonMobile number. Only returned when you ask for it by name, sourced from a LinkedIn profile URL, and billed per number actually found. UNVERIFIED: the source publishes no confidence, recency or line-type, so unlike an email address there is no deliverability verdict to attach. Check your obligations before dialling — a number being available is not a representation that it is lawful to call.
company_profilePersonThe employer's own record, nested under `company`: industry, headcount, founded year, description, annual revenue, total funding, latest round and monthly visits. One field rather than eight because one entity lookup answers all of them — you are billed once per company, not once per attribute, and people who share an employer share the lookup.

Defaults

A people request that names no fields resolves identity only: linkedin_url, title, seniority, department, location, company, company_domain, email. email is deliberately absent from that list — it is a different and far more expensive kind of work, and a default that quietly bought an address for every row would multiply the cost of a run several times over without the caller asking for anything.

A company request defaults to company, company_domain, location, country, headline. An explicit fields list always replaces the default rather than extending it.

Firmographics on a company row

The company entity carries these at no extra charge, so they come back with any company row and there is nothing to request: industry, employeeCount, employeeRange, foundedYear, logoUrl, revenueAnnualUsd, fundingTotalUsd, latestFundingRound, monthlyVisits.

Controlled vocabularies

FieldValues
seniorityfounder, c_level, vp, director, manager, senior, entry
departmentengineering, product, design, sales, marketing, finance, operations, people, legal, support, data, it, executive, other
emailStatusverified, probable, unverified, risky, undeliverable, unknown

Every address discovered by a run is put through a deliverability check and the outcome lands in emailStatus, so you can filter to the addresses that pass before anything reaches a sending tool. An address you supplied yourself is left exactly as given — we do not spend your credits checking data you already had and did not ask about.

Endpoints

Base URL /api/v1. Authenticate with Authorization: Bearer ek_live_… or x-api-key: ek_live_….

EndpointWhat it does
POST /api/v1/people/searchFind people matching an ideal-customer profile. `query` carries the natural-language intent; every filter is optional. `fields` defaults to identity only — no address is looked up unless you ask.
POST /api/v1/people/enrichResolve up to 100 identities — a LinkedIn URL, an email, or a name plus employer — into full person records. This is where work email addresses come from.
POST /api/v1/companies/searchFind companies matching firmographic filters and a natural-language query.
POST /api/v1/companies/enrichResolve up to 100 domains, names or LinkedIn URLs into company records.
GET /api/v1/jobs/{id}Poll a job. Always answers 200 — read `job.status`. `results` is null until the job is terminal, then pages with an opaque forward-only cursor.
GET /api/v1/openapiThis API as an OpenAPI 3.1 document, generated from the schemas the routes validate with. No key required.
Enrich two fields and an address for one person
curl -s https://www.enrichments.io/api/v1/people/enrich \
  -H 'authorization: Bearer ek_live_xxxxxxxxxxxxxxxxxxxx' \
  -H 'content-type: application/json' \
  -H 'Idempotency-Key: 2f8c1b90-7f3a-4c21-9a4e-6f0f2b1d5e77' \
  -d '{
    "people": [
      { "name": "Ada Lovelace", "companyDomain": "example.com" }
    ],
    "fields": ["title", "seniority", "email"]
  }'
  • A search accepts count from 1 to 100 and defaults to 25.
  • An enrichment accepts up to 100 rows per call and returns one row per input, in input order.
  • Small requests answer inline. Larger ones are queued and return a job id with results: null; poll GET /api/v1/jobs/{id} until the job is terminal. Polling is free.
  • Send an Idempotency-Key header on any submission. For 24 hours the same key with the same payload replays the original response rather than running and charging again.

The full machine-readable contract — every request and response schema, in OpenAPI 3.1 — is at /api/v1/openapi. It needs no API key and it is generated from the schemas the routes parse with, so it cannot drift from the implementation.

Errors

Every failure is a JSON envelope carrying a type, a human-readable message and a request_id that is also returned as an X-Request-Id response header.

StatusTypeMeans
400invalid_request_errorUnparseable body or an invalid parameter.
401authentication_errorMissing, revoked, expired or unknown API key.
402billing_errorNot enough credits. Nothing was queued and nothing was charged.
403permission_errorThe key is valid but lacks the required scope.
404not_found_errorNo such job in this workspace.
422invalid_request_errorThe request parsed but is not runnable — an invalid parameter, an unsupported enrichment field (including any phone field), or an idempotency key reused with a different payload.
429rate_limit_errorThe key's request quota is exhausted. See the Retry-After header.
500api_errorOur fault. Retry and quote the request id.
503api_errorThe upstream data source is unavailable. Retry shortly.

What gets charged

Credits are spent on delivered data only. An enrichment row is charged when at least one of the fields you asked for was resolved by us: a row that cannot be matched is free, a row that matches somebody but adds none of the requested fields is free, and an identifier you supplied that comes back unchanged was never a resolved field. A search is charged per record returned, because there the identity is the delivered unit.

Per-unit prices, plan allowances and the mechanism behind the rule are on pricing.

Boundaries

  • Mobile numbers are available as the `phone` field and billed per number found. 15 common aliases — mobile, cell, direct dial, telephone and the rest — resolve to it rather than failing. Unlike an email address a number carries no deliverability verdict, because nothing checks a phone the way a mailbox can be checked.
  • A phone number being available is not advice that it is lawful to call. Obligations under TCPA, DNC registries and local consent rules are the caller's.
  • The field catalogue is closed. A request for anything outside the 12 people fields is a validation error, not an empty column.
  • Email addresses are work addresses found from public sources. Personal contact details are not part of the catalogue.
  • Enrichment is live rather than a licensed static database, so the same query can legitimately return different data tomorrow.

Questions

What fields does the Enrichments API return?
People records can carry email, linkedin_url, title, seniority, department, location, country, company, company_domain, headline, phone, company_profile. Company records can carry company, company_domain, linkedin_url, location, country, headline, plus firmographics that ride along with any company row: industry, employeeCount, employeeRange, foundedYear, logoUrl, revenueAnnualUsd, fundingTotalUsd, latestFundingRound, monthlyVisits. Requesting anything outside those lists is a validation error rather than an empty column.
How is the API authenticated?
With an organization-scoped API key, sent as an Authorization: Bearer header or as x-api-key. Keys carry scopes, are shown in full exactly once at creation, and can be revoked or rotated at any time.
Is there an OpenAPI document?
Yes, at https://www.enrichments.io/api/v1/openapi, and no API key is needed to read it. It is OpenAPI 3.1 and is generated from the same schemas the routes validate requests with, so it cannot describe an API that is not served.
How are retries handled?
Send an Idempotency-Key header. The same key with the same payload replays the original response for 24 hours instead of running and charging again; the same key with a different payload is refused with a 422.