Ava Data

Contact Enrichment API

Contact Enrichment API — Append Phones, Emails & Related Persons to CRM Records

Send a name and address. Get back a verified phone, email, and — on Deep Search — the full related-persons graph. Integrate in minutes with a bearer token. Pay per match.

What is Contact Enrichment?

Contact enrichment is the process of appending missing or outdated contact information — phone numbers, email addresses, address history, related persons — to existing records in a CRM, database, or lead list. Instead of manually searching for contact details or buying expensive bulk data subscriptions, you call an enrichment API with the information you already have and receive the missing fields back in the response.

One scoping note before you read further, because it decides whether this is the contact enrichment API you want: it does not return job titles, employers, firmographics, or technographics. What it appends is residential contact data — phones, emails, address history, and related people such as spouses, relatives, and known associates — against a person record. If you are shopping for company-side enrichment, this is the wrong API, and you will save time knowing it now.

Ava Data's contact enrichment API specializes in person-level data: given a full name and a known address (or city and state), it returns the best current phone number, the best email address, and optionally the related-persons graph. This is PI-grade skip tracing delivered as a developer-friendly JSON API — the same underlying data used by professional investigators and skip trace vendors, available per match on the same $9/month plan as the dashboard — no separate API contract, no usage minimum.

Internal links: Skip Tracing API · Phone Number Lookup API · Bulk Skip Tracing · CRM Enrichment glossary · People Search API

What Fields Does the Contact Enrichment API Return?

Two endpoints, two response shapes. Every response is wrapped as { "success": true, "data": { ... } }, and every field below is defined in the OpenAPI spec the API reference is generated from.

Standard Search — POST /api/v1/standard-search

Field on dataTypeNotes
matchFoundbooleanCheck this before reading anything else.
creditsChargedintegerOne credit per data type charged when a match is found, two at most: ["phone"] = 1, ["email"] = 1, ["phone","email"] = 2. 0 on a miss.
phones[]arrayObjects of { number, type }. Present only when "phone" was requested and a match was found.
emails[]arrayObjects of { address }. Present only when "email" was requested and a match was found.
namestring, nullableThe matched person as Ava Data has them on file.
addresses[]arrayObjects of { street, city, state, zip }.
matchTypestring, nullableHow the record was matched on this call. The spec does not enumerate its values — read it, do not branch on a hard-coded list.

The same caution applies to phones[].type: it is a plain string describing the line — the spec's example is mobile — not a closed enum, so parse it defensively rather than switching on a fixed set of values.

Deep Search — POST /api/v1/deep-search

Field on dataTypeNotes
matchFoundbooleanSame contract as Standard Search.
creditsChargedinteger10 when a match is found, 0 otherwise.
subjectobjectfirstName, lastName, aliases[], age, dob, deceased, phones[], emails[], addresses[], bankruptcy.
relatedPeople[]arrayEach entry carries firstName, lastName, age, deceased, relationship, phones[], emails[], addresses[], bankruptcy.

relationship is the label describing the tie back to the subject — the spec's example is Spouse. Like matchType, it is a nullable string with no enumerated values, so display it, do not switch on it. Note the asymmetry too: aliases[] and dob exist on subject only, not on entries in relatedPeople[]. Deep Search takes no dataTypes parameter — it always returns the full shape.

One field the response does not carry: a persistent person identifier. No schema in the spec returns an ID you can store and re-query later, so when you use this as an identity resolution API the resolution key stays on your side — match the returned name and addresses back to your own record, and re-resolve on the next call rather than expecting a stable ID to join on.

What a no-match looks like

A no-match is not an error. The API returns HTTP 200 with matchFound: false and creditsCharged: 0, and nothing is billed for a miss. The spec defines phones and emails as present only when that data type was requested and a match was found, so on a miss they are not there to read at all. Branch on matchFound first and treat every array as optional — code that reaches straight for data.phones.length breaks on the first miss.

deceased and bankruptcy come back as public-record flags. Ava Data is not a consumer reporting agency and this data is not a consumer report: it may be used to locate and contact a person, never to decide eligibility for credit, insurance, employment, or housing.

Identity Resolution API vs. Ava Data's Identity Graph

Developers evaluating enrichment vendors often arrive looking for an identity resolution API, so it is worth being exact about the line, because the two categories solve different problems.

Identity resolution, as the term is used in martech and CDP work, means collapsing many records scattered across many systems into one canonical entity — deterministic and probabilistic matching that produces a persistent identifier which survives across calls, so you can maintain a golden record over time and recognize the same human on the next visit.

The Ava Data API does not do that, and does not return a persistent person ID. There is no entity key on subject or on relatedPeople[], there is no GET endpoint that retrieves a person by identifier, and nothing in a response lets you join two separate calls together as provably the same individual. The only identifiers in the spec belong to jobs and log entries — jobId, lookupId, and the activity id on GET /api/v1/usage. Every search is a stateless, point-in-time answer keyed by the identity you sent in. If you need a durable ID to deduplicate a warehouse or stitch profiles across systems, that is a customer data platform's job, not this API's.

What Ava Data does have is an identity graph, and it solves the adjacent problem: turning a partial record into a reachable person. The graph stores edges between people — shared addresses over time, shared surnames at the same property, and other public-record connections — and a Deep Search walks those edges outward from the subject. The input is a person, not an anonymous fragment: lastName is required on both /api/v1/standard-search and /api/v1/deep-search, and firstName, address, city, state, and zip narrow it to the right individual. What comes back is contact channels plus, on Deep Search, a labeled set of adjacent people. It expands outward from one person rather than collapsing many records into one.

Two response fields do real work if you are thinking in resolution terms, and neither appears on both endpoints. Standard Search returns a nullable matchType string on data, labeling the match made on that call. Deep Search returns subject.aliases[], a list of name variants you can use to reconcile spellings inside your own database. Both are per-call signals, not durable keys.

The practical pattern: keep your own record ID as the identity spine, call the API with what that record already holds, and write the result back onto it. Your CRM stays the system of record — for Follow Up Boss, our one live CRM integration does exactly this, writing enrichment results back to the lead as an append-only note.

Enriching at Scale — Synchronous and Batch

Both modes are documented endpoint by endpoint in the API reference.

Synchronous enrichment is the right pattern when a record has to be enriched the moment it enters your system — a new lead form submission, a CRM record created by a rep, an inbound inquiry that needs a callback number before the first follow-up. You POST to /api/v1/standard-search or /api/v1/deep-search and write the JSON straight onto the record. Note the required inputs: lastName is mandatory on both endpoints, and dataTypes is additionally mandatory on Standard Search. Address components are optional but are what sharpen the match, so send everything the record has.

Batch enrichment handles backfill — an entire CRM export, a stale prospect database, a list that needs phones before a campaign. It is a three-call async flow, and it runs Standard Search only; Deep Search is a single-search endpoint with no bulk equivalent.

POST /api/v1/bulk/upload
curl -X POST https://app.avadata.ai/api/v1/bulk/upload \
  -H "Authorization: Bearer sk_live_your_api_key_here" \
  -F "file=@leads.csv" \
  -F 'dataTypes=["phone","email"]'
200 Response
{
  "success": true,
  "data": {
    "jobId": "abc123-def456",
    "lookupId": "xyz789",
    "recordCount": 500,
    "dataTypes": ["phone", "email"],
    "creditsPerMatch": 2,
    "status": "processing"
  }
}

Three details catch people out. The upload is multipart/form-data, not JSON. dataTypes is sent as a JSON array string in the form field, not as a repeated field. And the CSV needs a header row plus at minimum an address column — firstName, lastName, city, state and zip columns are what make the matching sharper.

Then poll GET /api/v1/bulk/{jobId}/status. status moves through uploadingqueuedprocessingcompleted, or lands on failed with an errorMessage. The response also carries progress (0–100) and a nullable estimatedSecondsRemaining you can use to set your next poll interval instead of hammering a fixed timer — keys are rate limited to 60 requests per minute, and that ceiling covers your polling as well as your lookups. There are no webhooks in v1.1.0 — polling is the only completion signal, so build for it rather than waiting on a callback that will not arrive.

When the job completes, GET /api/v1/bulk/{jobId}/download returns numRecords, numMatches, creditsCharged, and a results array that mirrors your uploaded columns with the appended data alongside them — so your original row keys survive the round trip and the join back into your database is trivial. Add ?format=csv if you would rather have the file back than the JSON. Credits are reserved upfront at the quoted creditsPerMatch and then adjusted down to actual matches once processing finishes, so the reservation is a ceiling, not a bill.

The two modes share the same data and the same per-match billing, so mix them freely — synchronous lookups for new records, batch jobs for periodic refresh — with no separate contract and no separate pricing tier. See Bulk Skip Tracing for the batch workflow end to end.

Pricing

Lookup typeCreditsPrice per matchReturns
Standard — Phone Only1$0.02Best verified phone number
Standard — Phone + Email2$0.04Phone + email address
Deep Search10$0.20Phones, emails, related persons (spouses, siblings, associates)

Credits are only charged when a match is returned. No match = no charge. See full plan details →

Authenticating and Operating the Contact Enrichment API

Keys are generated from Settings → API Access and carry an sk_live_ prefix, sent as Authorization: Bearer sk_live_.... An administrator has to enable API access on the account before any key will work — that is the most common cause of a 401 on a key that looks perfectly well formed.

The rate limit is 60 requests per minute, per key. That ceiling is what decides which of the two enrichment modes a job belongs in: synchronous calls are for records arriving one at a time, and a 10,000-record backfill run synchronously would take just under three hours pinned at the limit. Send that job to /bulk/upload instead — a multipart upload of the CSV plus a dataTypes value, and one request against the limit rather than ten thousand.

Every failure returns the same envelope, so a contact enrichment API client can parse errors with one code path:

402 Error response
{
  "success": false,
  "error": "Insufficient credits for the requested operation."
}
StatusWhat it meansWhat to do
400Invalid or missing request parameters.Usually a missing or empty lastName, or a missing dataTypes on Standard Search. Not retryable — fix the payload.
401Missing, invalid, or inactive API key, or API access not enabled.Check the key, then check that an admin has switched API access on for the account.
402Insufficient credits.Top up. Pre-flight long runs against GET /credits and read availableCredits — bulk jobs reserve credits upfront and settle to actual matches afterward.
403Account suspended, or access denied to the requested resource.Also what you get requesting a job that does not belong to your account.
404The requested resource does not exist.In practice a bad jobId on a bulk or audience endpoint. An enrichment search that finds nobody is a 200 with matchFound: false, never a 404.
429Over 60 requests per minute on this key.Back off and retry. Version 1.1.0 of the spec defines no retry header, so use your own exponential backoff.
500Internal server error.Retryable. Any credits charged are refunded on the search endpoints, so a retry does not leave you double-billed for the failure.

On retries and idempotency: version 1.1.0 defines no idempotency key. A 429 did no work and a 500 refunds on the search endpoints, so both are safe to retry. The case to handle yourself is a client-side timeout on a request the server actually completed — retrying that one can bill a second match. Key your own outbound calls by your CRM record ID, and reconcile with GET /usage, whose recentActivity rows carry the endpoint, status code, credits used, and timestamp for each request.

Every status code above is generated from the same OpenAPI spec that powers the full API reference, where each endpoint lists the codes it can actually return.

Frequently Asked Questions

Does the contact enrichment API return a persistent person ID?

No. Ava Data performs identity resolution at query time rather than exposing a persisted entity you can address later. Neither subject nor relatedPeople[] carries an entity identifier, and none of the ten endpoints retrieves a person by ID — the only identifiers the API hands back, jobId and lookupId on a bulk upload, address a job rather than a person. Each search is a stateless, point-in-time answer keyed by the identity you send, so two calls about the same person cannot be joined by anything Ava Data returns. If you need identity resolution that persists across calls, keep your own record ID as the spine and write results back onto it.

Can I enrich a record from an address alone?

Not through the synchronous endpoints. lastName is required on both /standard-search and /deep-search. Address, city, state and zip are optional inputs that sharpen the match, but a name is the key the search is built on. If you are starting from a property address with no name attached, get the owner name first — public assessor and deed records carry it — then enrich.

What happens when there is no match?

You get HTTP 200 with data.matchFound: false and data.creditsCharged: 0. Credits are charged only when a match is returned, so misses cost nothing. Branch on data.matchFound before reading anything else: on a Standard Search miss the phones and emails arrays are absent from data rather than returned empty, so indexing into them without checking is what breaks first.

Does the rate limit apply to bulk polling too?

Yes. The 60 requests per minute is per API key across every endpoint — searches, uploads, status polls and downloads all draw on the same budget and all return 429 when it is exceeded. That is the argument for moving large backfills onto /bulk/upload: one request covers the whole file instead of one request per record, leaving the budget free for the status polls that follow.

Are there webhooks or an idempotency key?

The v1.1.0 API emits no outbound callbacks and accepts no idempotency key. Bulk job completion is discovered by polling GET /bulk/{jobId}/status, using the returned progress and estimatedSecondsRemaining to pace your interval rather than polling on a fixed tight loop. Retrying is safe on both documented failure classes — a 429 means nothing was processed, and on a 500 any credits charged by a search endpoint are refunded — but back off between attempts, and because there is no idempotency key, deduplicate retried timeouts on your side. If you want event-driven enrichment without writing a poller, the Follow Up Boss integration triggers a trace from a CRM webhook instead.

How to Integrate

Authentication is a bearer token in the Authorization header. No OAuth flow, no SDK, no required library — anything that can send an HTTP POST works. Keys are generated in Settings → API Access once an administrator has enabled API access on the account, and each key is rate limited to 60 requests per minute; exceed it and the endpoint returns 429. Here is a complete, copy-pasteable synchronous contact enrichment call:

POST /api/v1/standard-search
curl -X POST https://app.avadata.ai/api/v1/standard-search \
  -H "Authorization: Bearer sk_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Robert",
    "lastName": "Chen",
    "address": "1400 Harbor Blvd",
    "city": "Fullerton",
    "state": "CA",
    "zip": "92832",
    "dataTypes": ["phone", "email"]
  }'
200 Response
{
  "success": true,
  "data": {
    "matchFound": true,
    "creditsCharged": 2,
    "phones": [
      { "number": "7145550198", "type": "mobile" }
    ],
    "emails": [
      { "address": "rchen@example.com" }
    ],
    "name": "Robert Chen",
    "addresses": [
      { "street": "1400 Harbor Blvd", "city": "Fullerton", "state": "CA", "zip": "92832" }
    ]
  }
}

lastName and dataTypes are the only required fields; the address components narrow the match. Swap the path for /api/v1/deep-search and drop dataTypes to get the subject object plus the relatedPeople array back instead. For backfills, the same data is available through the asynchronous bulk endpoints — upload, poll status, download — covered on the Bulk Skip Tracing page. Two account-level GETs handle metering: GET /api/v1/credits returns availableCredits alongside the subscription, wallet and reserved balances behind it, and GET /api/v1/usage returns request and credit totals with a paginated recentActivity log.

The full endpoint reference — every parameter, response field and status code — is at the API reference, generated from the OpenAPI spec. To have API access enabled on your account, email support@avadata.ai.

Start enriching your CRM records

Integrate in minutes. Pay only for matches.

Get API Access →