API Reference
Ava Data API Reference
Skip trace one record or a 250,000-row file, build property lists, and turn contact lists into ad audience files, over REST. Searches charge credits only when they return a match, so a miss costs nothing. This page is generated from the OpenAPI spec, which is synced from the live API at every build.
Make your first call
Create an account
Sign up at app.avadata.ai and start your plan. API access is on for every account.
Generate a key
In the dashboard, open Settings → API Access and create a key. It starts with sk_live_.
Check it, then search
Call GET /credits (free) to confirm the key works, then POST /standard-search with a name and address.
Get Your API Key → Code samples: curl, Node.js, Python, Postman
Base URL and authentication
All endpoints are relative to https://app.avadata.ai/api/v1. Authenticate with a
bearer token in the Authorization header:
Authorization: Bearer sk_live_your_api_key_here Send your key in the Authorization header exactly as Authorization: Bearer sk_live_...: the word Bearer with a capital B, one space, then the whole key. Keys are sk_live_ followed by 64 hexadecimal characters. Any other form, such as a lower-case "bearer", returns 401 "Invalid or missing API key".
Create keys in the app at Settings → API Access. A key is shown once, when you create it, and can't be shown again. Keys have no scopes and don't expire. An account can have several active keys, so to rotate one: create a new key, deploy it, then revoke the old key in Settings; a revoked key is refused from its next request. If API access is turned off for the account, calls return 401 "API access is not enabled for this account"; contact support.
Keep keys on your server. The API sends no CORS headers, so it can't be called from a web page. Each key is rate limited to 60 requests per minute (see the API description).
Verify a key with the cheapest call in the API — reading your credit balance costs nothing:
curl -X GET https://app.avadata.ai/api/v1/credits \
-H "Authorization: Bearer sk_live_your_api_key_here"
Every endpoint example below is generated from the spec.
Where the spec supplies an example value, that value is shown; where it
does not, the field shows its type, and a nullable field with no example
shows null. Optional and conditional fields are printed at
their full shape — the tables say when each one is actually returned.
API access is on for every account. If a key returns
401, generate a new one from Settings → API Access in
the dashboard; if it still fails, email
support@avadata.ai.
Overview and rules
Ava Data's REST API for skip tracing (Deep Search, Standard Search and bulk CSV files), Digital Matching (ad-platform audience files) and List Builder (property lists). Use it from your CRM, AI agents or your own software.
Base URL: https://app.avadata.ai/api/v1. Every path in this file is relative to it, so POST /deep-search is https://app.avadata.ai/api/v1/deep-search. This file is served without a key at GET /openapi.json as a file download with no CORS headers, so browser-based tools can't import it by URL: download it and import the file.
Before you start: you need an active or trialing plan (searches, uploads and List Builder previews, pulls and skip traces return 403 with code SUBSCRIPTION_REQUIRED otherwise, even with wallet credits), an API key from Settings → API Access, and available credits. Call the API from your server: it sends no CORS headers, so requests from a web page fail, and a key must never be shipped to a browser. Send the key exactly as Authorization: Bearer sk_live_.... JSON requests need Content-Type: application/json and a body of at most 1 MB (413 with code BODY_TOO_LARGE). CSV uploads are multipart/form-data with the file in a field named file, up to 100 MB (413 with code FILE_TOO_LARGE).
No test mode: there is no sandbox and no test key. Every call runs against live data and spends real credits. The cheapest real test: GET /credits (free) confirms your key works, POST /lists/preview is free, and a few Standard Searches with dataTypes set to ["phone"] on records you already know cost 1 credit per match and nothing when there's no match.
Billing. Read creditsCharged in each response rather than computing it from matchFound.
- Deep Search: 10 credits when the result includes at least one phone, email or related person. A match that returns only an address or identity costs 0, and so does no match. The search needs 10 available credits to start.
- Standard Search: on a match, 1 credit for each data type in
dataTypes, even when that type comes back empty. No match costs 0. List each data type once: a repeated type is charged again. - Bulk uploads and list skip traces: credits are reserved up front (rows × data types). When the job finishes, each row is charged 1 credit for each requested data type actually returned, and the rest is released.
- Digital Matching:
creditsPerMatchcredits for each matched contact, every selected platform included. Every uploaded row is reserved up front. - List Builder: 1 credit per property record pulled into a list. The filter catalog, previews, reading records and downloading a list are free.
Credits: availableCredits on GET /credits (total minus reserved) is what you can spend. reservedCredits are held by running bulk jobs, Digital Matching jobs, list pulls and skip traces until they finish. Subscription credits are spent before wallet credits.
Errors: every error body is JSON {"success": false, "error": "..."}, sometimes with code, field or extra fields (see the Error schema). Many errors carry no code (every 401, search 402s and most upload 400s), so branch on the HTTP status first and use code, when present, to tell cases apart. A 500 from a search refunds the credits it took and is safe to retry after a short wait.
Rate limit: 60 requests per API key in a fixed 60-second window that starts with the key's first request. Every request made with the key counts, including errors, status polls and downloads; GET /openapi.json and requests refused before the key is checked (401s, INVALID_JSON, BODY_TOO_LARGE and unknown paths) don't. Every other response carries X-RateLimit-Limit (60), X-RateLimit-Remaining (requests left in this window) and X-RateLimit-Reset (when the window resets, as a Unix timestamp in seconds). Request 61 gets 429 with code RATE_LIMITED, a Retry-After header (whole seconds until the window resets, at least 1) and retryAfter, limit and resetsAt in the body: wait Retry-After seconds, then retry. Counts are kept per server instance, so treat X-RateLimit-Remaining as a guide and always honor Retry-After. List Builder also has per-account limits; their 429s carry their own codes, some send Retry-After, and the X-RateLimit-* headers don't describe them.
Retries and timeouts: searches and CSV uploads accept no idempotency key, so retrying a request that timed out on your side can charge a second search or start a second job. Before retrying one, compare availableCredits and reservedCredits on GET /credits with the values from before the request. POST /lists and POST /lists/{listId}/skip-trace accept an Idempotency-Key header. A search can wait up to a minute before it runs when the service is busy, so give searches a client timeout of at least 90 seconds.
Jobs: bulk, Digital Matching and list jobs run in the background. Poll their status every 5-10 seconds (each poll counts toward the rate limit); there are no webhooks or callbacks. Bulk and Digital Matching results stay downloadable until the job is deleted from History in the app.
Versioning: changes within /api/v1 are additive. New endpoints, optional request fields, response fields, error codes and enum values can appear at any time, so ignore fields and codes you don't recognize. A change that could break an existing integration is announced at least 30 days ahead.
Changes in 1.2.2: county lists match each county by its Census county code instead of by name. Requests and responses keep their shape, but counts can change: an independent city such as Baltimore city now finds its homes, and a county that shares its name (Baltimore County) counts only its own. Connecticut county lists use its 8 counties (Fairfield, Hartford, Litchfield, Middlesex, New Haven, New London, Tolland and Windham) instead of the 9 planning regions. GET /lists and GET /lists/{listId} can show lists made in the app from street addresses or a map area; their filters.location.kind is address, radius or polygon (see ListResource filters), and lists made in the app can also show ranges.estimatedValue (the estimated home value, in dollars). POST /lists and POST /lists/preview don't take those kinds or ranges.estimatedValue yet and answer 400.
Changes in 1.2.1: the per-key 429 now sends a Retry-After header, code RATE_LIMITED and retryAfter, limit and resetsAt in the body, and its error text says how long to wait. Every response to a request whose key is accepted, errors included, carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
Changes in 1.2.0: added the List Builder endpoints (/lists), layout=standard on GET /bulk/{jobId}/download, the Idempotency-Key header on the two List Builder POSTs, and the 409 and 503 responses. Documented the 413 responses, the error codes and the request limits. Corrected the documented Deep Search and Standard Search response fields (phones, emails, addresses, name) and the bulk CSV input rules to match what the API already returns and accepts; those endpoints behave as before. Response data objects are now named schemas (for example DeepSearchResult, StandardSearchResult and BulkJobStatus), so generated clients get readable type names; the JSON is unchanged.
Support: Support@avadata.ai. Responses carry no request ID, so include the endpoint, the time (UTC) and the HTTP status when you write in.
Permitted use: see section 3 of the Terms of Service (https://avadata.ai/terms/). Ava Data is not a consumer reporting agency, and the information it provides is not a consumer report under the Fair Credit Reporting Act. You may not use it to establish eligibility for credit or insurance, employment, housing or tenancy, a government licence or benefit, or any other purpose regulated by the FCRA. Ava Data does not screen numbers against do-not-call registries, and responses have no do-not-call or consent field. You are solely responsible for complying with the TCPA, federal and state do-not-call rules and every other law that governs contacting people, including obtaining any consent those laws require.
This specification is also published at https://avadata.ai/openapi.json, where browser-based tools can import it by URL, and rendered as a reference at https://avadata.ai/docs/. Pricing: 1 credit costs 2 cents. Every account, API access included, is on the $9/month plan, which includes 100 credits, and there is no order minimum.
What a call costs
Billing is per match, not per request. A search that returns nothing charges zero credits, and on a server error any charged credits are refunded for search endpoints.
| Call | Credits | Cost | Charged when |
|---|---|---|---|
| Standard Search — phone or email | 1 | 2¢ | A match is found |
| Standard Search — phone + email | 2 | 4¢ | A match is found, even if one of the two comes back empty |
| Deep Search — adds related persons | 10 | 20¢ | The result includes a phone, an email or a related person |
| Bulk upload or list skip trace | 1 per data type | 2¢ each | That data type is returned for the record |
| Digital Matching | 2 per contact | 4¢ | A contact is matched; every selected platform is included |
| List Builder pull | 1 per record | 2¢ | A record is pulled into the list |
| Credits, usage, job status, previews, filter catalog, records and downloads | 0 | Free | Never (each call still counts toward the rate limit) |
Bulk jobs, Digital Matching jobs and list pulls reserve their maximum
when they start and release the unused part when they finish, so
unmatched rows are released rather than billed. On Standard Search,
dataTypes isn't de-duplicated: list each type once, because
a repeated type is charged again.
Search
Single-record skip trace searches. The result comes back in the response. Integrating skip tracing into a real-estate app, CRM, or internal tool is one authenticated POST from your server: send a name and address to /standard-search or /deep-search, and write the returned phone numbers and email addresses onto the record. API access is included on every account.
Deep Search
Looks up one person by name and returns their phones, emails and addresses, plus related people (relatives and associates) with their phone numbers.
Input: only lastName is validated as required, but a last name on its own can match too many people. Send firstName with a ZIP code, or with a city and state, and the street address when you have it. A search that matches too many people returns 400 with code INSUFFICIENT_INPUT (credits refunded). Deep Search looks up people: a company, LLC or trust name sent as lastName usually finds no match, so send an owner's personal name. Fields that aren't in the schema are ignored, not refused, so check the spelling (firstName, not first_name). Validation errors name only the first problem and have no field: a missing lastName, or a body sent without Content-Type: application/json, returns 400 "Required".
Cost: 10 credits, taken when the search starts (402 when fewer than 10 are available) and refunded unless the result includes at least one phone, email or related person. So matchFound: true with creditsCharged: 0 is a normal result. A 500 refunds the credits and is safe to retry after a short wait.
Example request
curl -X POST https://app.avadata.ai/api/v1/deep-search \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"firstName": "John",
"lastName": "Smith",
"address": "123 Main St",
"city": "Austin",
"state": "TX",
"zip": "78701"
}' Example response
{
"success": true,
"data": {
"matchFound": true,
"subject": {
"firstName": "John",
"lastName": "Smith",
"aliases": [
"John A Smith"
],
"age": 45,
"deceased": false,
"phones": [
{
"number": "5125551234",
"lineType": "Mobile",
"disconnected": false
},
{
"number": "5125559876",
"lineType": "Landline",
"disconnected": false
}
],
"emails": [
"john.smith@example.com"
],
"addresses": [
{
"address": "123 Main St",
"city": "Austin",
"state": "TX",
"zip": "78701-1234"
}
],
"bankruptcy": false
},
"relatedPeople": [
{
"firstName": "Jane",
"lastName": "Smith",
"age": 43,
"deceased": false,
"relationship": "Spouse",
"phones": [
{
"number": "5125554321",
"lineType": "Mobile",
"disconnected": false
}
],
"emails": [],
"addresses": [],
"bankruptcy": false
}
],
"creditsCharged": 10
}
} Request body — application/json
| Field | Type | Required | Notes |
|---|---|---|---|
firstName | string | no | First name. Optional, but most searches need it to narrow the match to one person. Example: "John" |
lastName | string | yes | Last name. Required. Deep Search looks up people, so a company, LLC or trust name usually finds no match; send an owner's personal name. Example: "Smith" |
address | string | no | Street address. Example: "123 Main St" |
city | string | no | City. Example: "Austin" |
state | string | no | Two-letter state code. Example: "TX" |
zip | string | no | ZIP code. Example: "78701" |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
matchFound | boolean | true when a person record matched. A match can still have no phones or emails; see creditsCharged. Example: true |
subject | object | Present only when matchFound is true: the person the search matched. Optional fields are left out when the source has no value; they are never null. Example: {"firstName":"John","lastName":"Smith","aliases":["John A Smith"],"age":45,"deceased":false,"phones":[{"number":"5125551234","lineType":"Mobile","disconnected":false},{"number":"5125559876","lineType":"Landline","disconnected":false}],"emails":["john.smith@example.com"],"addresses":[{"address":"123 Main St","city":"Austin","state":"TX","zip":"78701-1234"}],"bankruptcy":false} |
relatedPeople | array of object | Present only when matchFound is true: relatives and associates of the subject. Can be empty. |
creditsCharged | integer | 10 when the result includes at least one phone, email or related person; otherwise 0, including matches that return only an address or identity. Total your spend from this field, not from matchFound. Example: 10 |
Error responses: 400, 401, 402, 403, 413, 429, 500. See error codes below.
Standard Search
Looks up one person by name and address and returns phones, emails or both, chosen with dataTypes.
Input: lastName and dataTypes are the only fields validated as required, but the search needs a name and a complete address: street plus ZIP, or city and state. When the details are too incomplete for the search to run, the call returns 400 with code INSUFFICIENT_INPUT (credits refunded). Fields that aren't in the schema are ignored, not refused. Validation errors name only the first problem and have no field: a missing lastName or dataTypes, or a body sent without Content-Type: application/json, returns 400 "Required".
Cost: on a match, 1 credit for each type in dataTypes (["phone"] or ["email"] = 1, ["phone","email"] = 2), charged even when a requested type comes back empty. No match costs 0. List each type once: the API doesn't remove repeats, so ["phone","phone"] is charged 2 credits on a match. The credits are taken when the search starts (402 when not enough are available). A 500 refunds them and is safe to retry after a short wait.
Example request
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": "John",
"lastName": "Smith",
"address": "123 Main St",
"city": "Austin",
"state": "TX",
"zip": "78701",
"dataTypes": [
"phone",
"email"
]
}' Example response
{
"success": true,
"data": {
"matchFound": true,
"phones": [
{
"number": "5125551234",
"lineType": "Mobile"
},
{
"number": "5125559876",
"lineType": "Landline"
}
],
"emails": [
"john.smith@example.com"
],
"name": {
"first": "John",
"last": "Smith"
},
"addresses": [
{
"address": "123 Main St",
"city": "Austin",
"state": "TX",
"zip": "78701",
"zip4": "1234"
}
],
"matchType": "Individual",
"creditsCharged": 2
}
} Request body — application/json
| Field | Type | Required | Notes |
|---|---|---|---|
firstName | string | no | First name. Example: "John" |
lastName | string | yes | Last name. Required. Example: "Smith" |
address | string | no | Street address. Send it with zip, or with city and state. Example: "123 Main St" |
city | string | no | City. Example: "Austin" |
state | string | no | Two-letter state code. Example: "TX" |
zip | string | no | ZIP code. Example: "78701" |
dataTypes | array of "phone" | "email" | yes | Which contact data to return, and the price on a match: ["phone"] = 1 credit, ["email"] = 1 credit, ["phone","email"] = 2 credits, charged even when a requested type comes back empty. List each type once: the API doesn't remove repeats, so ["phone","phone"] is charged 2 credits. Example: ["phone","email"] |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
matchFound | boolean | true when a person or household matched. Example: true |
phones | array of object | Present only when "phone" was requested and a match was found. Can be empty. Ordered with up to 2 mobile numbers first, then 1 landline, then the rest. |
emails | array of string | Present only when "email" was requested and a match was found. Plain strings. Can be empty. |
name | object | Present only when matchFound is true: the matched name. A part is omitted when the source has none. Example: {"first":"John","last":"Smith"} |
addresses | array of object | Present only when matchFound is true: the matched address. Zero or one entry today. Example: [{"address":"123 Main St","city":"Austin","state":"TX","zip":"78701","zip4":"1234"}] |
matchType | string | Present only when matchFound is true: Individual or Household. Individual means the contact data matched the named person. Household means it matched at the household level, so a phone or email may belong to another person at the same address. Example: "Individual" |
creditsCharged | integer | On a match, the number of data types requested (1 credit each), even when a type came back empty; a repeated type counts again. 0 when there is no match. Example: 2 |
Error responses: 400, 401, 402, 403, 413, 429, 500. See error codes below.
Bulk
Skip trace a CSV file as a background job: upload, poll the status, then download the results with layout=standard. Batch skip tracing runs as three calls on files of up to 250,000 rows: upload the CSV, poll the job status, then download the matched phone numbers and email addresses as JSON or CSV.
Upload CSV for bulk processing
Upload a CSV of people to skip trace for phones, emails or both as a background job. Returns a jobId: poll GET /bulk/{jobId}/status, then download with GET /bulk/{jobId}/download?layout=standard.
File: the CSV goes in a multipart field named file (100 MB maximum; larger files return 413 with code FILE_TOO_LARGE, and any other multipart problem returns 400 with code INVALID_UPLOAD; a request that isn't multipart, or has no file part, returns 400 "CSV file is required..." with no code). It is read as UTF-8 (from Excel, save as CSV UTF-8; other encodings garble accented names), and the first line must be the header row. Rows are split on line breaks before quotes are read, so remove line breaks inside cells (for example two-line addresses) first. Blank lines are ignored. Up to 250,000 data rows per file, counting rows that are later skipped. A larger file returns 400 with maxBatchSize and recordCount; rely on maxBatchSize, since the cap can change.
Columns: headers are matched case-insensitively, ignoring surrounding spaces, against these exact names. Other headers, such as first_name, zip_code or address1, aren't used for searching (column mapping is a feature of the app's upload screen, not the API). If two columns match the same field, the first is used.
- Street address: address, street, street address
- First name: first name, firstname, first
- Last name: last name, lastname, last
- Full name, used only when the file has no first-name or no last-name column: name, full name, fullname, owner name, owner
- City: city
- State: state, st
- ZIP: zip, zipcode, zip code, postal code
Rows: each row needs one of these combinations to be searched: First + Last Name + ZIP; First + Last Name + City + State; or Street Address + City + State + ZIP. A full-name value counts as First + Last Name, except company names (LLC, Inc. and similar), which need the full street address, city, state and ZIP. A file whose columns can't meet any combination returns 400 ("The CSV columns can't produce any matches."), so an address column on its own is not enough. Rows that meet none are skipped: they aren't searched, reserved or charged, and the response reports them in skippedCount and skippedReason. If no row qualifies, the call returns 400 with invalidCount.
Joining results: results hold only the rows that were searched, with your own columns first, and carry no row number. Add an ID column of your own (any header not listed above) so you can join results back to your data. recordCount is the number of rows searched: the data rows minus skippedCount.
Cost: recordCount × creditsPerMatch credits are reserved now (402 with needed, available, reserved and total when they aren't available). When the job finishes, each row is charged 1 credit for each requested data type actually returned (with ["phone","email"], a row that returns a phone but no email costs 1), and the rest of the reservation is released. List each data type once: a repeated type isn't charged twice at the end, but it is counted twice in the reservation and in creditsPerMatch.
Retries: there is no idempotency key, so retrying an upload creates a second job and a second reservation. If an upload times out, check reservedCredits on GET /credits before you send it again.
Example request
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"]' Example response
{
"success": true,
"data": {
"jobId": "abc123-def456",
"lookupId": "xyz789",
"recordCount": 497,
"dataTypes": [
"phone",
"email"
],
"creditsPerMatch": 2,
"status": "processing",
"skippedCount": 3,
"skippedReason": "3 row(s) were skipped because they don't include enough info to be searchable (they won't be charged). Each row needs at least one of these combinations to be searchable: First + Last Name + ZIP; First + Last Name + City + State; Street Address + City + State + ZIP. A single full-name column counts as First + Last Name, except for company names (LLC, Inc., etc.), which need the full street address, city, state, and ZIP.",
"message": "Processing 497 records (3 skipped — not enough info to be searchable; skipped rows are never charged). Use GET /api/v1/bulk/abc123-def456/status to check progress."
}
} Request body — multipart/form-data
| Field | Type | Required | Notes |
|---|---|---|---|
file | string | yes | The CSV file: UTF-8, a header row, then at least one data row. For example: Contact ID,First Name,Last Name,Address,City,State,Zip C-1001,John,Smith,123 Main St,Austin,TX,78701 Example: "leads.csv" |
dataTypes | string | yes | JSON array string of the data to append: ["phone"], ["email"] or ["phone","email"]. List each type once. Example: "[\"phone\",\"email\"]" |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
jobId | string | Use with GET /bulk/{jobId}/status and GET /bulk/{jobId}/download. Example: "abc123-def456" |
lookupId | string | The job's entry in History in the app. No API endpoint takes it. Example: "xyz789" |
recordCount | integer | Rows that will be searched: the data rows minus skippedCount. Example: 497 |
dataTypes | array of "phone" | "email" | The data types you sent, as sent (repeats are kept). Example: ["phone","email"] |
creditsPerMatch | integer | The number of data types you sent, repeats included. recordCount × creditsPerMatch credits are reserved; the final charge is 1 credit for each requested data type actually returned per row. Example: 2 |
status | string | Always "processing" in this response. The first status checks can still show "uploading" or "queued". Example: "processing" |
skippedCount | integer | Present only when rows were skipped: rows that met no input combination. They aren't searched, charged or included in the results. Example: 3 |
skippedReason | string | Present only when rows were skipped: why, with the accepted combinations. Example: "3 row(s) were skipped because they don't include enough info to be searchable (they won't be charged). Each row needs at least one of these combinations to be searchable: First + Last Name + ZIP; First + Last Name + City + State; Street Address + City + State + ZIP. A single full-name column counts as First + Last Name, except for company names (LLC, Inc., etc.), which need the full street address, city, state, and ZIP." |
message | string | A readable summary. Don't parse it. Example: "Processing 497 records (3 skipped — not enough info to be searchable; skipped rows are never charged). Use GET /api/v1/bulk/abc123-def456/status to check progress." |
Error responses: 400, 401, 402, 403, 413, 429, 500. See error codes below.
Check bulk job status
Poll a bulk job (a CSV upload or a list skip trace) every 5-10 seconds; each poll counts toward the 60-per-minute limit, and there are no webhooks. status moves through uploading, queued and processing to completed or failed. Once it is completed, download with GET /bulk/{jobId}/download?layout=standard; the final charge is creditsCharged in that response (this one doesn't include it).
failed is usually final. Once processing has started, a problem on our side doesn't fail a job: it stays in progress (a status other than completed or failed), with its credits still reserved, until processing ends. Rarely, a job that reported failed turns out to be still processing or already finished: it moves back to processing (its credits are reserved again) or to completed, and its matches are charged like any other job, whether or not you keep checking. So check the status once more before you upload the same file again. A status check can take a few seconds when it is the one that finds this out.
Example request
curl -X GET https://app.avadata.ai/api/v1/bulk/abc123-def456/status \
-H "Authorization: Bearer sk_live_your_api_key_here" Example response
{
"success": true,
"data": {
"jobId": "abc123-def456",
"status": "processing",
"progress": 65,
"estimatedSecondsRemaining": 120,
"numRecords": 497,
"numMatches": 312,
"errorMessage": ""
}
} Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
jobId | path | string | yes | The job ID returned by the bulk upload endpoint or by POST /lists/{listId}/skip-trace. |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
jobId | string | Example: "abc123-def456" |
status | "uploading" | "queued" | "processing" | "completed" | "failed" | uploading, queued or processing while the job runs, then completed or failed. Treat any value you don't recognize as still running. Rarely, a failed job moves back to processing, so check once more before you upload the same file again. Example: "processing" |
progress | integer | Percent complete (0-100). Example: 65 |
estimatedSecondsRemaining | integer | Rough time left in seconds, or null when there is no estimate. Example: 120 |
numRecords | integer | Rows in the job. Example: 497 |
numMatches | integer | Rows matched so far; final once status is completed. Example: 312 |
errorMessage | string | An empty string ("") unless status is failed; then a short reason. Example: "" |
Error responses: 401, 403, 404, 429, 500. See error codes below.
Download bulk job results
Download the results of a completed bulk job as JSON (default) or CSV. Send layout=standard on every download. The job must be completed (otherwise 400 "Job is not completed yet. Current status: ..."). Results stay available until the job is deleted from History in the app; after that the job no longer exists and the call returns 404 "Job not found". 400 "No results available for download" means a completed job has no stored results. Rows skipped at upload are not in the results.
Standard layout: results of a list skip trace (POST /lists/{listId}/skip-trace) always use the standard layout, and layout=standard gives any bulk job the same layout. Each row has your own columns first, in their original order (for POST /lists/{listId}/skip-trace: Owner Name, Owner 1 First Name, Owner 1 Last Name, Owner 2 First Name, Owner 2 Last Name, Mailing Address, Mailing City, Mailing State, Mailing Zip, Property Address, Property City, Property State, Property Zip and Property ID, so you can join results back to the list), then these result columns:
Match Found: Yes or No.Phone 1toPhone 6, each followed byLine Type 1toLine Type 6(for example Mobile or Landline).Mobile 1toMobile 3andLandline 1toLandline 3: the same numbers grouped by line type.Email 1,Email 2and so on, when email was requested.
Empty values are empty strings. In JSON, results is an array of row objects keyed by column name and columns lists the column names in order. In CSV, the header row uses the same names, and cells that start with =, +, @ (or - when the value isn't a number) get a leading ' so spreadsheets don't run them as formulas. If one of your columns has the same name as another column, a number is added to it, like Phone 1 (2), and a blank column name becomes Column N (N is its position).
Without layout=standard, CSV upload jobs keep their original response shape, where results is usually the results file as one CSV text string in a different column layout. It is kept only so existing integrations don't break; use layout=standard for every integration. Digital Matching jobs aren't available in the standard layout (404); download them with GET /audience/{jobId}/download.
Example request
curl -X GET "https://app.avadata.ai/api/v1/bulk/abc123-def456/download?layout=standard" \
-H "Authorization: Bearer sk_live_your_api_key_here" Example response
{
"success": true,
"data": {
"jobId": "abc123-def456",
"numRecords": 497,
"numMatches": 312,
"creditsCharged": 401,
"columns": [
"Owner Name",
"Property ID",
"Match Found",
"Phone 1",
"Line Type 1"
],
"results": [
{
"Owner Name": "Jane Sample",
"Property ID": "prop_3f9a1c2e7b4d8e6f0a1b2c3d",
"Match Found": "Yes",
"Phone 1": "6025550123",
"Line Type 1": "Mobile",
"Mobile 1": "6025550123",
"Email 1": "jane@example.com"
}
]
}
} Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
jobId | path | string | yes | The job ID returned by the bulk upload endpoint or by POST /lists/{listId}/skip-trace. |
format | query | "json" | "csv" | no | Response format: json (the default) or csv, in lower case. Any value other than csv returns JSON. |
layout | query | "standard" | no | Send standard (recommended for every integration) for the standard layout described above. List skip-trace results always use it, with or without this parameter. Any other value returns 400 with field: layout. |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
jobId | string | Example: "abc123-def456" |
numRecords | integer | Rows in the job. Example: 497 |
numMatches | integer | Rows matched. Example: 312 |
creditsCharged | integer | Final credits charged for the job. Example: 401 |
columns | array of string | Standard layout only: the column names in order (your columns, then Match Found, Phone 1, Line Type 1 … Landline 3, Email 1 …). Example: ["Owner Name","Property ID","Match Found","Phone 1","Line Type 1"] |
results | object | Per-record results. An array of row objects in the standard layout; see the endpoint description for when the original shape is returned instead. Example: [{"Owner Name":"Jane Sample","Property ID":"prop_3f9a1c2e7b4d8e6f0a1b2c3d","Match Found":"Yes","Phone 1":"6025550123","Line Type 1":"Mobile","Mobile 1":"6025550123","Email 1":"jane@example.com"}] |
Error responses: 400, 401, 403, 404, 429, 500. See error codes below.
Digital Matching
Turn a contact CSV into ready-to-upload customer-list files for major ad platforms. Each platform's file holds hashed identifiers in that platform's format. Uploads return 403 when Digital Matching is turned off for the account.
Upload CSV to build ad audiences
Upload a contact CSV and receive ready-to-upload customer-list files for the selected ad platforms. Each contact is matched against Ava Data's identity graph, and each match gets hashed identifiers in each platform's format (often several emails and phone numbers per person). Returns a jobId: poll GET /audience/{jobId}/status, then download with GET /audience/{jobId}/download.
Access: Digital Matching must be turned on for the account; otherwise the call returns 403 "Digital Matching access is not enabled for this account" (no code).
File: the CSV goes in a multipart field named file (100 MB maximum; larger files return 413 with code FILE_TOO_LARGE, and any other multipart problem returns 400 with code INVALID_UPLOAD; a request that isn't multipart, or has no file part, returns 400 "CSV file is required..." with no code). It is read as UTF-8 (from Excel, save as CSV UTF-8; other encodings garble accented names), and the first line must be the header row. Rows are split on line breaks before quotes are read, so remove line breaks inside cells (for example two-line addresses) first. Blank lines are ignored. Up to 50,000 data rows per file. A larger file returns 400 with maxBatchSize and recordCount; rely on maxBatchSize, since the cap can change.
Columns: headers are matched case-insensitively, ignoring surrounding spaces, against these exact names; other headers aren't used for matching.
- Email: email, e-mail, email address, emailaddress
- Phone: phone, phone number, phonenumber, mobile, cell, cell phone
- Street address: address, street, street address
- First name: first name, firstname, first
- Last name: last name, lastname, last
- Full name, used only when the file has neither a first-name nor a last-name column: full name, fullname, name, owner name, owner
- City: city
- State: state, st
- ZIP: zip, zipcode, zip code, postal code
The columns must allow at least one of: email; phone; street + city + state + ZIP; name + city + state; or name + ZIP, where name is first and last name columns or a full-name column. Otherwise the call returns 400. Rows aren't checked one by one at upload: every data row counts in recordCount and is reserved, and rows that can't be matched are never charged.
Cost: creditsPerMatch credits for each matched contact, every selected platform included. Read the price from creditsPerMatch in the response rather than hard-coding it. recordCount × creditsPerMatch credits are reserved now (402 with needed, available, reserved and total when they aren't available); when the job finishes, matched contacts are charged and the rest is released. There is no idempotency key, so retrying an upload creates a second job.
By uploading you confirm you have the right to use the list for advertising and will follow each ad platform's customer-list policies.
Example request
curl -X POST https://app.avadata.ai/api/v1/audience/upload \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-F 'file=@contacts.csv' \
-F 'platforms=["facebook","google"]' Example response
{
"success": true,
"data": {
"jobId": "abc123-def456",
"lookupId": "xyz789",
"recordCount": 500,
"platforms": [
"facebook",
"google"
],
"creditsPerMatch": 2,
"status": "processing",
"message": "Processing 500 records. Use GET /api/v1/audience/abc123-def456/status to check progress."
}
} Request body — multipart/form-data
| Field | Type | Required | Notes |
|---|---|---|---|
file | string | yes | The CSV file: UTF-8, a header row, then at least one data row. Example: "contacts.csv" |
platforms | string | yes | JSON array string of one or more target platforms: "facebook", "google", "linkedin", "tiktok", "generic". Repeats are removed. Example: "[\"facebook\",\"google\"]" |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
jobId | string | Use with GET /audience/{jobId}/status and GET /audience/{jobId}/download. Example: "abc123-def456" |
lookupId | string | The job's entry in History in the app. No API endpoint takes it. Example: "xyz789" |
recordCount | integer | Every data row in the file. Rows aren't checked one by one at upload. Example: 500 |
platforms | array of "facebook" | "google" | "linkedin" | "tiktok" | "generic" | The selected platforms, repeats removed. Example: ["facebook","google"] |
creditsPerMatch | integer | Credits charged for each matched contact, every selected platform included. recordCount × creditsPerMatch credits are reserved now. Read the price here rather than hard-coding it. Example: 2 |
status | string | Always "processing" in this response. Example: "processing" |
message | string | A readable summary. Don't parse it. Example: "Processing 500 records. Use GET /api/v1/audience/abc123-def456/status to check progress." |
Error responses: 400, 401, 402, 403, 413, 429, 500. See error codes below.
Check Digital Matching job status
Poll a Digital Matching job every 5-10 seconds; each poll counts toward the 60-per-minute limit, and there are no webhooks. Only Digital Matching job IDs work here (404 for other jobs). There is no time estimate. creditsCharged and platformCounts are final once status is completed. As with bulk jobs, a job that reported failed can, rarely, move back to processing (its credits are reserved again) or to completed, so check the status once more before you upload the same file again.
Example request
curl -X GET https://app.avadata.ai/api/v1/audience/abc123-def456/status \
-H "Authorization: Bearer sk_live_your_api_key_here" Example response
{
"success": true,
"data": {
"jobId": "abc123-def456",
"status": "processing",
"progress": 65,
"numRecords": 500,
"numMatches": 312,
"creditsCharged": 624,
"platforms": [
"facebook",
"google"
],
"platformCounts": {
"facebook": 298,
"google": 305
},
"errorMessage": ""
}
} Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
jobId | path | string | yes | The job ID returned by the audience upload endpoint. |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
jobId | string | Example: "abc123-def456" |
status | "uploading" | "queued" | "processing" | "completed" | "failed" | uploading, queued or processing while the job runs, then completed or failed. Treat any value you don't recognize as still running. Rarely, a failed job moves back to processing, so check once more before you upload the same file again. Example: "processing" |
progress | integer | Percent complete (0-100). Example: 65 |
numRecords | integer | Rows in the job. Example: 500 |
numMatches | integer | Matched contacts (each charged creditsPerMatch). Final once status is completed. Example: 312 |
creditsCharged | integer | Credits charged for the job. Final once status is completed. Example: 624 |
platforms | array of string | The job's platforms. Example: ["facebook","google"] |
platformCounts | object | Matched contacts per platform. null until the job completes. Example: {"facebook":298,"google":305} |
errorMessage | string | An empty string ("") unless status is failed; then a short reason. Example: "" |
Error responses: 401, 403, 404, 429, 500. See error codes below.
Download audience files
Download a completed Digital Matching job (otherwise 400 "Job is not completed yet"). By default you get a ZIP named digital_matching_{jobId}.zip that holds:
- One CSV per selected platform, with the identifiers hashed (SHA-256) in that platform's format: facebook_instagram_custom_audience.csv, google_ads_customer_match.csv, linkedin_contact_list.csv, tiktok_customer_file.csv and generic_hashed_audience.csv. A platform with no matched contacts has no file in the ZIP.
- master_audience_data.csv: your contacts as you uploaded them, with every appended identifier. Keep it for reference and auditing; it isn't meant for upload.
- README.txt: how to upload each file.
Pass platform to download one platform's CSV instead. It must be one of the job's platforms (400 "Unknown platform for this job" otherwise), and a platform with no matched contacts returns 200 with an empty body. Results stay available until the job is deleted from History in the app; after that the job no longer exists and the call returns 404 "Job not found".
Example request
curl -X GET https://app.avadata.ai/api/v1/audience/abc123-def456/download \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-o audienceDownload.zip Example response
By default a successful call returns
application/zip — a file rather than a JSON
body — so the request above writes it straight to disk with
-o.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
jobId | path | string | yes | The job ID returned by the audience upload endpoint. |
platform | query | "facebook" | "google" | "linkedin" | "tiktok" | "generic" | no | Download one of the job's platform CSVs instead of the full ZIP. |
Error responses: 400, 401, 403, 404, 429, 500. See error codes below.
List Builder
Build property lists from the same filters as the List Builder page: area, property type, owner type, quick lists (distress and equity signals), timing and ranges. Lists run as background jobs: create, poll, then read or download the records.
Get filter options
Returns every value the List Builder filters object accepts: location types and states, quick lists (with the timing windows each one owns), owner, property, loan and lien types, timing presets, Probate & Death Records options, range fields, and the record columns you can request. Use the ids from this catalog in filters and columns. Free. Returns 403 when List Builder isn't turned on for the account.
Example request
curl -X GET https://app.avadata.ai/api/v1/lists/filters \
-H "Authorization: Bearer sk_live_your_api_key_here" Example response
{
"success": true,
"data": {
"location": {
"types": [
"city",
"county",
"zip"
],
"maxValues": 50,
"states": [
"AL",
"AK",
"AZ",
"AR",
"CA",
"CO",
"CT",
"DE",
"FL",
"GA",
"HI",
"ID",
"IL",
"IN",
"IA",
"KS",
"KY",
"LA",
"ME",
"MD",
"MA",
"MI",
"MN",
"MS",
"MO",
"MT",
"NE",
"NV",
"NH",
"NJ",
"NM",
"NY",
"NC",
"ND",
"OH",
"OK",
"OR",
"PA",
"RI",
"SC",
"SD",
"TN",
"TX",
"UT",
"VT",
"VA",
"WA",
"WV",
"WI",
"WY",
"DC"
]
},
"quicklists": [
{
"id": "absentee-owner",
"label": "Absentee Owner",
"description": "Owners whose mailing address differs from the property — they don't live there.",
"timing": []
},
{
"id": "involuntary-lien",
"label": "Involuntary Lien",
"description": "Properties with a lien filed against them (judgment, HOA, mechanic's, etc.).",
"timing": [
"lienRecordedDays"
]
}
],
"quicklistMatch": [
"any",
"all"
],
"maxQuicklists": 10,
"ownerTypes": [
{
"id": "individual",
"label": "Individual"
},
{
"id": "company",
"label": "Company"
},
{
"id": "trust",
"label": "Trust"
}
],
"propertyTypes": [
{
"id": "single-family",
"label": "Single Family"
},
{
"id": "multi-family-2-4",
"label": "Multi-Family (2–4)"
},
{
"id": "apartments-5plus",
"label": "Apartments (5+)"
},
{
"id": "condo-townhouse",
"label": "Condo / Townhouse"
},
{
"id": "mobile-manufactured",
"label": "Mobile / Manufactured"
},
{
"id": "commercial",
"label": "Commercial"
}
],
"loanTypes": [
{
"id": "conventional",
"label": "Conventional"
},
{
"id": "fha",
"label": "FHA"
},
{
"id": "va",
"label": "VA"
}
],
"lienTypes": [
{
"id": "federal-tax",
"label": "Federal Tax Lien"
},
{
"id": "state-tax",
"label": "State Tax Lien"
}
],
"distressTiming": {
"auctionNextDays": {
"presetDays": [
7,
14,
30,
60,
90
],
"direction": "next",
"customDates": true,
"requiresQuickList": [
"preforeclosure",
"notice-of-default"
]
},
"foreclosureFiledDays": {
"presetDays": [
30,
60,
90,
180,
365
],
"direction": "past",
"customDates": true,
"requiresQuickList": [
"preforeclosure",
"notice-of-default"
]
},
"lienRecordedDays": {
"presetDays": [
30,
90,
180,
365
],
"direction": "past",
"customDates": true,
"requiresQuickList": [
"involuntary-lien"
]
},
"expiredDays": {
"presetDays": [
7,
14,
30,
60,
90
],
"direction": "past",
"customDates": true,
"requiresQuickList": [
"expired-listing"
]
},
"soldDays": {
"presetDays": [
30,
60,
90,
180,
365
],
"direction": "past",
"customDates": true,
"requiresQuickList": [
"recently-sold"
]
}
},
"filings": {
"kinds": [
{
"id": "probate",
"label": "Probate filings"
},
{
"id": "death",
"label": "Death records"
},
{
"id": "both",
"label": "Both"
}
],
"presetDays": [
30,
60,
90,
180,
365
],
"maxLookbackDays": 365,
"unavailableStates": [
"AK",
"CT",
"DC",
"DE",
"MA",
"MD",
"ME",
"ND",
"NH",
"NJ",
"RI",
"VT"
],
"cannotCombineWith": [
"recently-sold",
"distressTiming.soldDays",
"distressTiming.lienRecordedDays",
"lienTypes"
]
},
"ranges": [
{
"field": "beds",
"unit": "bedrooms"
},
{
"field": "yearBuilt",
"unit": "year"
},
{
"field": "assessedValue",
"unit": "US dollars (county assessed value)"
}
],
"maxRecordsLimit": 100000,
"creditsPerRecord": 1,
"columns": [
{
"key": "propStreet",
"label": "Property Address",
"category": "Location",
"type": "string",
"default": true
},
{
"key": "ownerOccupied",
"label": "Owner Occupied",
"category": "Owner",
"type": "boolean",
"default": true
},
{
"key": "auctionDate",
"label": "Auction Date",
"category": "Foreclosure",
"type": "date",
"default": false
}
],
"maxColumns": 35
}
} Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
location | object | Example: {"types":["city","county","zip"],"maxValues":50,"states":["AL","AK","AZ","AR","CA","CO","CT","DE","FL","GA","HI","ID","IL","IN","IA","KS","KY","LA","ME","MD","MA","MI","MN","MS","MO","MT","NE","NV","NH","NJ","NM","NY","NC","ND","OH","OK","OR","PA","RI","SC","SD","TN","TX","UT","VT","VA","WA","WV","WI","WY","DC"]} |
quicklists | array of object | Every quick list, with the timing keys it owns. Example: [{"id":"absentee-owner","label":"Absentee Owner","description":"Owners whose mailing address differs from the property — they don't live there.","timing":[]},{"id":"involuntary-lien","label":"Involuntary Lien","description":"Properties with a lien filed against them (judgment, HOA, mechanic's, etc.).","timing":["lienRecordedDays"]}] |
quicklistMatch | array of string | Accepted values for filters.quicklistMatch. Example: ["any","all"] |
maxQuicklists | integer | The most quick lists one list can use. Example: 10 |
ownerTypes | array of object | Example: [{"id":"individual","label":"Individual"},{"id":"company","label":"Company"},{"id":"trust","label":"Trust"}] |
propertyTypes | array of object | Example: [{"id":"single-family","label":"Single Family"},{"id":"multi-family-2-4","label":"Multi-Family (2–4)"},{"id":"apartments-5plus","label":"Apartments (5+)"},{"id":"condo-townhouse","label":"Condo / Townhouse"},{"id":"mobile-manufactured","label":"Mobile / Manufactured"},{"id":"commercial","label":"Commercial"}] |
loanTypes | array of object | Example: [{"id":"conventional","label":"Conventional"},{"id":"fha","label":"FHA"},{"id":"va","label":"VA"}] |
lienTypes | array of object | Example: [{"id":"federal-tax","label":"Federal Tax Lien"},{"id":"state-tax","label":"State Tax Lien"}] |
distressTiming | object | One entry per timing key: its preset day counts, whether it looks back (past) or ahead (next), whether custom dates are accepted, and the quick lists it needs. Example: {"auctionNextDays":{"presetDays":[7,14,30,60,90],"direction":"next","customDates":true,"requiresQuickList":["preforeclosure","notice-of-default"]},"foreclosureFiledDays":{"presetDays":[30,60,90,180,365],"direction":"past","customDates":true,"requiresQuickList":["preforeclosure","notice-of-default"]},"lienRecordedDays":{"presetDays":[30,90,180,365],"direction":"past","customDates":true,"requiresQuickList":["involuntary-lien"]},"expiredDays":{"presetDays":[7,14,30,60,90],"direction":"past","customDates":true,"requiresQuickList":["expired-listing"]},"soldDays":{"presetDays":[30,60,90,180,365],"direction":"past","customDates":true,"requiresQuickList":["recently-sold"]}} |
filings | object | Options for filters.filings (Probate & Death Records). Example: {"kinds":[{"id":"probate","label":"Probate filings"},{"id":"death","label":"Death records"},{"id":"both","label":"Both"}],"presetDays":[30,60,90,180,365],"maxLookbackDays":365,"unavailableStates":["AK","CT","DC","DE","MA","MD","ME","ND","NH","NJ","RI","VT"],"cannotCombineWith":["recently-sold","distressTiming.soldDays","distressTiming.lienRecordedDays","lienTypes"]} |
ranges | array of object | Every key accepted in filters.ranges, with its unit. Example: [{"field":"beds","unit":"bedrooms"},{"field":"yearBuilt","unit":"year"},{"field":"assessedValue","unit":"US dollars (county assessed value)"}] |
maxRecordsLimit | integer | The most records one list can pull. Example: 100000 |
creditsPerRecord | integer | Credits per record pulled. Example: 1 |
columns | array of object | Every record column you can request, in catalog order. Example: [{"key":"propStreet","label":"Property Address","category":"Location","type":"string","default":true},{"key":"ownerOccupied","label":"Owner Occupied","category":"Owner","type":"boolean","default":true},{"key":"auctionDate","label":"Auction Date","category":"Foreclosure","type":"date","default":false}] |
maxColumns | integer | The most columns one request can ask for. Example: 35 |
Error responses: 401, 403, 429, 500. See error codes below.
Preview a list
Counts the properties that match your filters and shows what a pull would cost, without pulling or charging anything. Send the same filters you plan to send to POST /lists. Previews are free, but they share the account's limit of 20 per minute with the List Builder page (429 with code PREVIEW_MINUTE_LIMIT and a Retry-After: 60 header) and are capped at 250 per account per UTC day (429 with code PREVIEW_DAILY_LIMIT, plus limit, resetsAt and a Retry-After header in seconds). Invalid filters return 400 with field naming the bad path, for example filters.location.state. If a match count can't be fetched, the call returns 503 with code PREVIEW_UNAVAILABLE; try again.
Example request
curl -X POST https://app.avadata.ai/api/v1/lists/preview \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"location": {
"state": "AZ",
"kind": "county",
"values": [
"Maricopa"
]
},
"quicklists": [
"absentee-owner",
"high-equity"
],
"quicklistMatch": "all",
"propertyTypes": [
"single-family"
],
"ranges": {
"yearBuilt": {
"min": 1960
}
}
},
"maxRecords": 500
}' Example response
{
"success": true,
"data": {
"matchCount": 1842,
"creditsPerRecord": 1,
"maxRecordsLimit": 100000,
"maxRecords": 500,
"recordsToPull": 500,
"estimatedCredits": 500,
"creditsToReserve": 500
}
} Request body — application/json
| Field | Type | Required | Notes |
|---|---|---|---|
filters | object | yes | The filters to count. Use the same filters you plan to send to POST /lists. Example: {"location":{"state":"AZ","kind":"county","values":["Maricopa"]},"quicklists":["absentee-owner","high-equity"],"quicklistMatch":"all","propertyTypes":["single-family"],"ranges":{"yearBuilt":{"min":1960}}} |
maxRecords | integer | no | Optional cap to estimate with (1-100,000). Without it, the estimate uses the match count, up to 100,000. Example: 500 |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
matchCount | integer | Properties that match the filters. Example: 1842 |
creditsPerRecord | integer | Credits per record pulled. Example: 1 |
maxRecordsLimit | integer | The most records one list can pull. Example: 100000 |
maxRecords | integer | The cap used for this estimate: your maxRecords, or the match count up to 100,000. Example: 500 |
recordsToPull | integer | Records a pull would return: the smaller of matchCount and maxRecords. Example: 500 |
estimatedCredits | integer | Credits a pull of recordsToPull would charge. Example: 500 |
creditsToReserve | integer | Credits POST /lists would reserve for this maxRecords. Unused credits are released. Example: 500 |
Error responses: 400, 401, 403, 413, 429, 500, 503. See error codes below.
Create a list
Starts a list pull in the background and returns the list with status processing. maxRecords credits (1 per record) are reserved when the list is created. When the pull finishes you are charged for the records actually pulled and the rest is released; a pull that stops early keeps, and charges, the records it already pulled. Tip: preview first and set maxRecords to the preview's recordsToPull so you only hold the credits you need. Poll GET /lists/{listId} until status is completed or failed. An account can have 5 lists pulling at once (429 with code ACTIVE_PULL_LIMIT and limit). Not enough credits returns 402 with needed, available, reserved and total. Send an Idempotency-Key header to retry safely: the same key and body within 24 hours replays the original response (header Idempotent-Replayed: true) without creating a second list or charging again; the same key with a different body returns 400 with code IDEMPOTENCY_KEY_REUSED; a retry while the first request is still running returns 409 with code IDEMPOTENCY_IN_PROGRESS.
Example request
curl -X POST https://app.avadata.ai/api/v1/lists \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Maricopa absentee high equity",
"maxRecords": 500,
"filters": {
"location": {
"state": "AZ",
"kind": "county",
"values": [
"Maricopa"
]
},
"quicklists": [
"absentee-owner",
"high-equity"
],
"quicklistMatch": "all",
"propertyTypes": [
"single-family"
],
"ranges": {
"yearBuilt": {
"min": 1960
}
}
}
}' Example response
{
"success": true,
"data": {
"listId": "8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31",
"name": "Maricopa absentee high equity",
"type": "search",
"status": "processing",
"progress": 0,
"recordCount": 0,
"maxRecords": 500,
"creditsReserved": 500,
"creditsCharged": 0,
"filters": {
"location": {
"state": "AZ",
"kind": "county",
"values": [
"Maricopa"
]
},
"quicklists": [
"absentee-owner",
"high-equity"
],
"quicklistMatch": "all",
"propertyTypes": [
"single-family"
],
"loanTypes": [],
"lienTypes": [],
"ranges": {
"yearBuilt": {
"min": 1960
}
}
},
"errorMessage": null,
"createdVia": "api",
"createdAt": "2026-10-26T15:04:05.000Z",
"updatedAt": "2026-10-26T15:04:05.000Z",
"completedAt": null
}
} Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key | header | string | no | Optional. A key you choose (1-255 visible ASCII characters, no spaces) that makes retries safe. The same key and body within 24 hours replays the original response. |
Request body — application/json
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | A name for the list (1-120 characters). Example: "Maricopa absentee high equity" |
maxRecords | integer | yes | The most records to pull (1-100,000). This many credits are reserved up front; set it to the preview's recordsToPull to hold only what you need. Example: 500 |
filters | object | yes | Which properties to pull. Example: {"location":{"state":"AZ","kind":"county","values":["Maricopa"]},"quicklists":["absentee-owner","high-equity"],"quicklistMatch":"all","propertyTypes":["single-family"],"ranges":{"yearBuilt":{"min":1960}}} |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
listId | string | The list's ID. Example: "8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31" |
name | string | The list's name. Example: "Maricopa absentee high equity" |
type | string | "search" for lists built from filters; "stack" for combined lists made in the app. New values may be added later. Example: "search" |
status | string | "processing", "completed" or "failed". "completed" and "failed" are set only once credits are final. New values may be added later; treat an unknown value as "processing". Example: "processing" |
progress | integer | Percent complete (0-100). Example: 0 |
recordCount | integer | Records pulled so far. Example: 0 |
maxRecords | integer | The most records this list will pull. Example: 500 |
creditsReserved | integer | Credits held for this list right now: the full reservation while it is processing, then 0 once it finishes (the charge moves to creditsCharged and the unused part is released). Example: 500 |
creditsCharged | integer | Credits charged for records pulled. Final once status is "completed" or "failed". Example: 0 |
filters | object | The filters the list was built with, as saved: the same fields as the ListFilters request object (state upper-case, empty filter lists filled in). Fields may be added later. null for stacked lists. A list made in the app can also have a location the API doesn't take yet (street addresses or a map area; see location) and ranges.estimatedValue (the estimated home value, in dollars, as {min, max}), which the API doesn't take yet either. Example: {"location":{"state":"AZ","kind":"county","values":["Maricopa"]},"quicklists":["absentee-owner","high-equity"],"quicklistMatch":"all","propertyTypes":["single-family"],"loanTypes":[],"lienTypes":[],"ranges":{"yearBuilt":{"min":1960}}} |
errorMessage | string | null unless status is "failed". Then it is always: "The pull stopped before it finished. Records pulled before it stopped are kept and charged; the rest of the reserved credits were released." Example: null |
createdVia | string | "api" for lists made with an API key, "app" for lists made in the app. New values may be added later. Example: "api" |
createdAt | string | When the list was created (ISO 8601). Example: "2026-10-26T15:04:05.000Z" |
updatedAt | string | When the list last changed (ISO 8601). Example: "2026-10-26T15:04:05.000Z" |
completedAt | string | When the list finished, completed or failed (ISO 8601). null while status is processing. Example: null |
Error responses: 400, 401, 402, 403, 409, 413, 429, 500. See error codes below.
List your lists
Returns your lists, newest first. Lists made in the app appear too (createdVia: "app"), including combined lists made in the app (type: "stack"). Page through them with limit and offset.
Example request
curl -X GET https://app.avadata.ai/api/v1/lists \
-H "Authorization: Bearer sk_live_your_api_key_here" Example response
{
"success": true,
"data": {
"lists": [
{
"listId": "8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31",
"name": "Maricopa absentee high equity",
"type": "search",
"status": "processing",
"progress": 0,
"recordCount": 0,
"maxRecords": 500,
"creditsReserved": 500,
"creditsCharged": 0,
"filters": {
"location": {
"state": "AZ",
"kind": "county",
"values": [
"Maricopa"
]
},
"quicklists": [
"absentee-owner",
"high-equity"
],
"quicklistMatch": "all",
"propertyTypes": [
"single-family"
],
"loanTypes": [],
"lienTypes": [],
"ranges": {
"yearBuilt": {
"min": 1960
}
}
},
"errorMessage": null,
"createdVia": "api",
"createdAt": "2026-10-26T15:04:05.000Z",
"updatedAt": "2026-10-26T15:04:05.000Z",
"completedAt": null
}
],
"pagination": {
"limit": 50,
"offset": 0,
"total": 12,
"hasMore": false
}
}
} Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | no | Lists per page. Defaults to 50; values above 100 are treated as 100. |
offset | query | integer | no | Number of lists to skip: a whole number from 0 to 10,000,000 (400 with field: offset otherwise). |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
lists | array of object | Your lists, newest first. |
pagination | object |
Error responses: 400, 401, 403, 429, 500. See error codes below.
Get a list
Returns one list with its status, progress and credits. While a pull runs, poll every 5-10 seconds; each poll counts toward your key's 60 requests per minute. status turns completed or failed only once credits are final, so creditsCharged is the final charge at that point. Returns 404 when the list doesn't exist and 403 when it belongs to another account.
Example request
curl -X GET https://app.avadata.ai/api/v1/lists/8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31 \
-H "Authorization: Bearer sk_live_your_api_key_here" Example response
{
"success": true,
"data": {
"listId": "8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31",
"name": "Maricopa absentee high equity",
"type": "search",
"status": "processing",
"progress": 0,
"recordCount": 0,
"maxRecords": 500,
"creditsReserved": 500,
"creditsCharged": 0,
"filters": {
"location": {
"state": "AZ",
"kind": "county",
"values": [
"Maricopa"
]
},
"quicklists": [
"absentee-owner",
"high-equity"
],
"quicklistMatch": "all",
"propertyTypes": [
"single-family"
],
"loanTypes": [],
"lienTypes": [],
"ranges": {
"yearBuilt": {
"min": 1960
}
}
},
"errorMessage": null,
"createdVia": "api",
"createdAt": "2026-10-26T15:04:05.000Z",
"updatedAt": "2026-10-26T15:04:05.000Z",
"completedAt": null
}
} Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string | yes | The list ID returned by POST /lists or GET /lists. |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
listId | string | The list's ID. Example: "8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31" |
name | string | The list's name. Example: "Maricopa absentee high equity" |
type | string | "search" for lists built from filters; "stack" for combined lists made in the app. New values may be added later. Example: "search" |
status | string | "processing", "completed" or "failed". "completed" and "failed" are set only once credits are final. New values may be added later; treat an unknown value as "processing". Example: "processing" |
progress | integer | Percent complete (0-100). Example: 0 |
recordCount | integer | Records pulled so far. Example: 0 |
maxRecords | integer | The most records this list will pull. Example: 500 |
creditsReserved | integer | Credits held for this list right now: the full reservation while it is processing, then 0 once it finishes (the charge moves to creditsCharged and the unused part is released). Example: 500 |
creditsCharged | integer | Credits charged for records pulled. Final once status is "completed" or "failed". Example: 0 |
filters | object | The filters the list was built with, as saved: the same fields as the ListFilters request object (state upper-case, empty filter lists filled in). Fields may be added later. null for stacked lists. A list made in the app can also have a location the API doesn't take yet (street addresses or a map area; see location) and ranges.estimatedValue (the estimated home value, in dollars, as {min, max}), which the API doesn't take yet either. Example: {"location":{"state":"AZ","kind":"county","values":["Maricopa"]},"quicklists":["absentee-owner","high-equity"],"quicklistMatch":"all","propertyTypes":["single-family"],"loanTypes":[],"lienTypes":[],"ranges":{"yearBuilt":{"min":1960}}} |
errorMessage | string | null unless status is "failed". Then it is always: "The pull stopped before it finished. Records pulled before it stopped are kept and charged; the rest of the reserved credits were released." Example: null |
createdVia | string | "api" for lists made with an API key, "app" for lists made in the app. New values may be added later. Example: "api" |
createdAt | string | When the list was created (ISO 8601). Example: "2026-10-26T15:04:05.000Z" |
updatedAt | string | When the list last changed (ISO 8601). Example: "2026-10-26T15:04:05.000Z" |
completedAt | string | When the list finished, completed or failed (ISO 8601). null while status is processing. Example: null |
Error responses: 401, 403, 404, 429, 500. See error codes below.
Get list records
Returns a page of property records from a finished list. Records are ready when the list's status is completed, or failed with records kept; otherwise the call returns 400 with code LIST_NOT_READY ("List is not ready yet. Current status: processing") or LIST_FAILED ("This list failed before any records were pulled."). Each record has a propertyId plus the columns you ask for, in catalog order. Values are typed: numbers as numbers; ownerOccupied, mailVacant, mtg1Heloc and activePreforeclosure as booleans; dates as YYYY-MM-DD; allLoanTypes and quickListFlags as arrays (quickListFlags holds quick-list ids); empty values as null. propType and the loan type columns use the same labels as the filter catalog (or "Other"); pool is true when the property has one. Other text columns, such as ownerType and foreclosureStatus, are free text with no fixed list of values. propertyId stays the same for a property across lists, so you can use it to dedupe. Reading records is free.
Example request
curl -X GET https://app.avadata.ai/api/v1/lists/8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31/records \
-H "Authorization: Bearer sk_live_your_api_key_here" Example response
{
"success": true,
"data": {
"listId": "8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31",
"columns": [
"propStreet",
"propCity",
"propState",
"propZip",
"ownerName",
"owner1First",
"owner1Last",
"owner2First",
"owner2Last",
"ownerOccupied",
"mailStreet",
"mailCity",
"mailState",
"mailZip",
"beds",
"baths",
"sqft",
"yearBuilt",
"assessedValue",
"quickListFlags"
],
"records": [
{
"propertyId": "prop_3f9a1c2e7b4d8e6f0a1b2c3d",
"propStreet": "1234 W Example St",
"propCity": "Phoenix",
"propState": "AZ",
"propZip": "85015",
"ownerName": "Jane Sample",
"owner1First": "Jane",
"owner1Last": "Sample",
"owner2First": null,
"owner2Last": null,
"ownerOccupied": false,
"mailStreet": "PO Box 100",
"mailCity": "Tempe",
"mailState": "AZ",
"mailZip": "85281",
"beds": 3,
"baths": 2,
"sqft": 1450,
"yearBuilt": 1978,
"assessedValue": 28400,
"quickListFlags": [
"absentee-owner",
"high-equity"
]
}
],
"pagination": {
"limit": 100,
"offset": 0,
"total": 500,
"hasMore": true
}
}
} Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string | yes | The list ID returned by POST /lists or GET /lists. |
limit | query | integer | no | Records per page. Defaults to 100; values above 500 are treated as 500. |
offset | query | integer | no | Number of records to skip: a whole number from 0 to 10,000,000 (400 with field: offset otherwise). |
columns | query | string | no | Comma-separated column keys from GET /lists/filters, up to 35. Repeated columns= parameters are also accepted. Leave it out for the 20 default columns: propStreet, propCity, propState, propZip, ownerName, owner1First, owner1Last, owner2First, owner2Last, ownerOccupied, mailStreet, mailCity, mailState, mailZip, beds, baths, sqft, yearBuilt, assessedValue, quickListFlags. Columns come back in catalog order. Available keys: apn, propStreet, propCity, propState, propZip, propZip4, county, latitude, longitude, subdivision, schoolDistrict, metroArea, censusTract, ownerName, owner1First, owner1Last, owner2First, owner2Last, owner2Name, ownerType, ownerOccupied, ownershipRights, ownershipStart, yearsOwned, mailStreet, mailCity, mailState, mailZip, mailCounty, mailVacant, propType, propCategory, landUse, beds, baths, fullBaths, sqft, grossSqft, yearBuilt, effectiveYearBuilt, stories, units, rooms, pool, garage, garageSpaces, fireplaces, airConditioning, heat, roof, exteriorWalls, construction, condition, sewer, water, lotAcres, lotSqft, zoning, legalDescription, assessedValue, marketValueTotal, marketValueLand, marketValueImprovements, assessmentYear, annualTax, taxYear, openLienBalance, openLienCount, allLoanTypes, mtg1Lender, mtg1LoanType, mtg1OriginalAmount, mtg1EstBalance, mtg1EstRate, mtg1EstPayment, mtg1Ltv, mtg1RecordingDate, mtg1TermMonths, mtg1Heloc, activePreforeclosure, foreclosureStatus, foreclosureRecordingDate, defaultDate, auctionDate, auctionTime, auctionLocation, trusteeName, trusteePhone, foreclosureLoanAmount, foreclosureLender, lienCount, lien1Type, lien1DocType, lien1RecordingDate, quickListFlags, recordUpdated. |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
listId | string | Example: "8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31" |
columns | array of string | The column keys in each record, in catalog order. propertyId is always included too. Example: ["propStreet","propCity","propState","propZip","ownerName","owner1First","owner1Last","owner2First","owner2Last","ownerOccupied","mailStreet","mailCity","mailState","mailZip","beds","baths","sqft","yearBuilt","assessedValue","quickListFlags"] |
records | array of object | One object per property: propertyId plus each requested column. A column's type is listed in GET /lists/filters; empty values are null. |
pagination | object |
Error responses: 400, 401, 403, 404, 429, 500. See error codes below.
Download a list as CSV
Streams every record in a finished list as a CSV file named after the list. The same ready rules and columns parameter apply as for GET /lists/{listId}/records. The header row uses column keys (propertyId first, then the chosen columns), so it stays stable for parsing. Values match the records JSON: booleans as true/false and multi-value columns joined with "; ". Cells that start with =, +, @ (or - when the value isn't a number) get a leading ' so spreadsheets don't run them as formulas. An account can run 2 downloads at a time (429 with code DOWNLOAD_LIMIT). Downloading is free.
Example request
curl -X GET https://app.avadata.ai/api/v1/lists/8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31/download \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-o downloadList.csv Example response
By default a successful call returns
text/csv — a file rather than a JSON
body — so the request above writes it straight to disk with
-o.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string | yes | The list ID returned by POST /lists or GET /lists. |
columns | query | string | no | Comma-separated column keys from GET /lists/filters, up to 35. Repeated columns= parameters are also accepted. Leave it out for the 20 default columns (the same defaults as GET /lists/{listId}/records). Columns come back in catalog order. Available keys: apn, propStreet, propCity, propState, propZip, propZip4, county, latitude, longitude, subdivision, schoolDistrict, metroArea, censusTract, ownerName, owner1First, owner1Last, owner2First, owner2Last, owner2Name, ownerType, ownerOccupied, ownershipRights, ownershipStart, yearsOwned, mailStreet, mailCity, mailState, mailZip, mailCounty, mailVacant, propType, propCategory, landUse, beds, baths, fullBaths, sqft, grossSqft, yearBuilt, effectiveYearBuilt, stories, units, rooms, pool, garage, garageSpaces, fireplaces, airConditioning, heat, roof, exteriorWalls, construction, condition, sewer, water, lotAcres, lotSqft, zoning, legalDescription, assessedValue, marketValueTotal, marketValueLand, marketValueImprovements, assessmentYear, annualTax, taxYear, openLienBalance, openLienCount, allLoanTypes, mtg1Lender, mtg1LoanType, mtg1OriginalAmount, mtg1EstBalance, mtg1EstRate, mtg1EstPayment, mtg1Ltv, mtg1RecordingDate, mtg1TermMonths, mtg1Heloc, activePreforeclosure, foreclosureStatus, foreclosureRecordingDate, defaultDate, auctionDate, auctionTime, auctionLocation, trusteeName, trusteePhone, foreclosureLoanAmount, foreclosureLender, lienCount, lien1Type, lien1DocType, lien1RecordingDate, quickListFlags, recordUpdated. |
Error responses: 400, 401, 403, 404, 429, 500. See error codes below.
Skip trace a list
Starts a bulk skip trace of a completed list and returns a bulk jobId. Each record's owner names and mailing address are traced (the property address when there's no mailing address); records with no address are skipped and counted in skippedCount. Pricing is the normal bulk skip-trace price: each record is charged 1 credit for each requested data type actually returned (phone = 1, email = 1), so a record with no match costs nothing. maxCredits is reserved now and adjusted to the phones and emails actually returned when the job finishes. Track the job with GET /bulk/{jobId}/status and, once it's completed, download results with GET /bulk/{jobId}/download (JSON rows) or GET /bulk/{jobId}/download?format=csv. Results always use the standard layout: the list's columns, including Property ID so you can join results back to the list, then Match Found, Phone 1-6 with Line Type 1-6, Mobile 1-3, Landline 1-3 and Email 1… (see GET /bulk/{jobId}/download). The list must be completed with at least one record (400 with code LIST_NOT_READY, LIST_FAILED or LIST_EMPTY). One skip trace can be set up at a time per account (429 with code SKIP_TRACE_IN_PROGRESS), and when set-up is busy you may get 429 with code SKIP_TRACE_BUSY and a Retry-After header. Not enough credits returns 402 with needed, available, reserved and total. Idempotency-Key works the same way as on POST /lists, scoped to this list.
Example request
curl -X POST https://app.avadata.ai/api/v1/lists/8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31/skip-trace \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"dataTypes": [
"phone",
"email"
]
}' Example response
{
"success": true,
"data": {
"jobId": "abc123-def456",
"listId": "8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31",
"recordCount": 497,
"skippedCount": 3,
"dataTypes": [
"phone",
"email"
],
"creditsPerMatch": 2,
"maxCredits": 994,
"status": "processing",
"message": "Skip trace started. Track it with GET /bulk/{jobId}/status and download results with GET /bulk/{jobId}/download?format=csv once it completes."
}
} Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string | yes | The list ID returned by POST /lists or GET /lists. |
Idempotency-Key | header | string | no | Optional. A key you choose (1-255 visible ASCII characters, no spaces) that makes retries safe, scoped to this list. The same key and body within 24 hours replays the original response. |
Request body — application/json
| Field | Type | Required | Notes |
|---|---|---|---|
dataTypes | array of "phone" | "email" | no | Which contact data to append: ["phone"] (the default), ["email"] or ["phone","email"]. Each data type costs 1 credit per match. Example: ["phone","email"] |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
jobId | string | Use with GET /bulk/{jobId}/status and GET /bulk/{jobId}/download. Example: "abc123-def456" |
listId | string | Example: "8f14e45f-ea8e-4a3b-9d2c-1b7e6f0a9c31" |
recordCount | integer | Records sent for skip tracing. Example: 497 |
skippedCount | integer | Records left out because they have no mailing or property address. Example: 3 |
dataTypes | array of "phone" | "email" | Example: ["phone","email"] |
creditsPerMatch | integer | The most one record can cost: the number of data types requested. Each record is charged 1 credit for each requested data type actually returned, so a phone-only result costs 1. Example: 2 |
maxCredits | integer | recordCount × creditsPerMatch, reserved now and adjusted to the phones and emails actually returned when the job finishes. Example: 994 |
status | string | Example: "processing" |
message | string | Example: "Skip trace started. Track it with GET /bulk/{jobId}/status and download results with GET /bulk/{jobId}/download?format=csv once it completes." |
Error responses: 400, 401, 402, 403, 404, 409, 413, 429, 500. See error codes below.
Account
Credit balance and API usage.
Check credit balance
Free. Returns your credit balance; use it to check that a key works. availableCredits is what searches, uploads and list pulls can spend right now: totalCredits (subscription plus wallet credits) minus reservedCredits, which running bulk jobs, Digital Matching jobs, list pulls and skip traces hold until they finish. Subscription credits are spent before wallet credits, and they are set to 0 when the plan is no longer active. A paused plan gets 403 with code SUBSCRIPTION_PAUSED here too.
Example request
curl -X GET https://app.avadata.ai/api/v1/credits \
-H "Authorization: Bearer sk_live_your_api_key_here" Example response
{
"success": true,
"data": {
"subscriptionCredits": 3000,
"walletCredits": 1500,
"reservedCredits": 500,
"totalCredits": 4500,
"availableCredits": 4000,
"subscriptionStatus": "active"
}
} Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
subscriptionCredits | integer | Credits from your plan. Spent first; set to 0 when the plan is no longer active. Example: 3000 |
walletCredits | integer | Wallet credits, outside the plan. Spent after subscription credits. Example: 1500 |
reservedCredits | integer | Credits held by running bulk jobs, Digital Matching jobs, list pulls and skip traces. They are charged or released when each one finishes. Example: 500 |
totalCredits | integer | subscriptionCredits + walletCredits. Example: 4500 |
availableCredits | integer | totalCredits − reservedCredits: what you can spend right now. Check this one. Example: 4000 |
subscriptionStatus | string | The plan's status, for example active, trialing, past_due, canceled or none. Searches, uploads and List Builder previews, pulls and skip traces need active or trialing (403 with code SUBSCRIPTION_REQUIRED otherwise). A paused plan gets 403 with code SUBSCRIPTION_PAUSED on every endpoint, this one included. Example: "active" |
Error responses: 401, 403, 404, 429, 500. See error codes below.
Get API usage
Free. Returns request counts plus a page of your recent requests, newest first.
Only requests made with an API key are logged, one row per request once its response is sent. Requests refused before that (401s, RATE_LIMITED 429s and paused-plan 403s) aren't logged. creditsUsed is the charge a search reported; every other request logs 0, including uploads, list creates and list skip traces, because their charge is settled when the job finishes. stats.totalCreditsUsed adds those final job charges.
Paging: new requests, your /usage calls included, are added at the top while you page, so rows can shift between pages; dedupe by id. There is no total, and hasMore is true whenever a page is full, so stop when a page has fewer rows than limit.
Example request
curl -X GET https://app.avadata.ai/api/v1/usage \
-H "Authorization: Bearer sk_live_your_api_key_here" Example response
{
"success": true,
"data": {
"stats": {
"totalRequests": 1250,
"totalCreditsUsed": 8400,
"requestsToday": 45,
"requestsThisMonth": 890
},
"recentActivity": [
{
"id": "4b8e2f6a-1c3d-4e5f-8a9b-0c1d2e3f4a5b",
"endpoint": "/api/v1/deep-search",
"method": "POST",
"statusCode": 200,
"creditsUsed": 10,
"timestamp": "2026-10-26T15:04:05.123Z"
}
],
"pagination": {
"limit": 50,
"offset": 0,
"hasMore": false
}
}
} Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | no | Rows per page. Defaults to 50; values above 100 are treated as 100. Must be a whole number of 1 or more (400 with field: limit otherwise). |
offset | query | integer | no | Rows to skip: a whole number from 0 to 10,000,000 (400 with field: offset otherwise). |
Response — every response is wrapped as { "success": true, "data": { … } }. The
fields below are what data contains.
| Field | Type | Notes |
|---|---|---|
stats | object | |
recentActivity | array of object | A page of logged requests, newest first. |
pagination | object |
Error responses: 400, 401, 403, 429, 500. See error codes below.
Error codes
Every error is JSON with "success": false and an
error message. Some errors add a machine-readable
code, a field naming the request field that
failed, or details such as needed and
available on a 402 or resumeAt on a paused
plan. Many errors have no code, so branch on the HTTP status
first and on code when it is there. The status tells you
whether to retry, fix the request, or top up credits.
{
"success": false,
"error": "The search needs a name and a complete address (street plus ZIP, or city and state). Credits have been refunded.",
"code": "INSUFFICIENT_INPUT"
} | Status | Meaning |
|---|---|
400 | The request can't be processed as sent; fix it before retrying (LIST_NOT_READY is the exception: retry once the list completes). Codes: INVALID_JSON, INSUFFICIENT_INPUT (Deep Search or Standard Search can't run with the details sent; credits refunded), INVALID_UPLOAD, and the List Builder codes LIST_NOT_READY, LIST_FAILED, LIST_EMPTY, UNSUPPORTED_CONTENT_TYPE, INVALID_IDEMPOTENCY_KEY and IDEMPOTENCY_KEY_REUSED. Many 400s have no code: search validation errors name only the first problem, with no field (for example "Required" when lastName is missing or the body wasn't sent as application/json), and most upload 400s explain the problem in error only. Uploads add maxBatchSize and recordCount when a file has too many rows, and invalidCount when no row can be searched. |
401 | The API key is missing, malformed, revoked, or can't be used. No code is sent; error is one of: "Invalid or missing API key" (no Authorization header, or not exactly "Bearer sk_live_..."), "Invalid or inactive API key" (wrong or revoked key), "API access is not enabled for this account" (API access is turned off for the account; contact support) or "Account is suspended". Don't retry until the key or account is fixed. |
402 | Not enough available credits (total minus reserved). Deep Search and Standard Search return just "Insufficient credits"; Deep Search needs 10 available credits to start, even when the result ends up free. Bulk upload, Digital Matching upload, POST /lists and list skip trace add needed, available, reserved and total. Add credits, or wait for running jobs to release reserved credits, then retry. |
403 | The account can't use this endpoint or resource. code SUBSCRIPTION_REQUIRED: no active or trialing plan (searches, uploads, and List Builder previews, pulls and skip traces). code SUBSCRIPTION_PAUSED: the plan is paused, on every endpoint; resumeAt, when sent, is when it resumes on its own. code LIST_BUILDER_DISABLED: List Builder isn't turned on for the account. No code: "Digital Matching access is not enabled for this account", or "Access denied" when the job or list belongs to another account. Don't retry until the account changes. |
404 | The job, list or path doesn't exist ("Job not found", "List not found" or "Not found"), the job was deleted from History in the app, or a job ID was sent to the wrong endpoint:a non-Digital Matching job to /audience/{jobId}/..., or a Digital Matching job to /bulk/{jobId}/download with layout=standard. Unknown paths and wrong HTTP methods also return 404, not 405. |
409 | A request with this Idempotency-Key is still in progress (code IDEMPOTENCY_IN_PROGRESS). Retry with the same key once the first request finishes. |
413 | The request is too large. code BODY_TOO_LARGE: a JSON body over 1 MB. code FILE_TOO_LARGE: an uploaded file over 100 MB. Send less data per request. |
429 | Too many requests. The per-key limit is 60 requests in a fixed 60-second window that starts with the key's first request, and every request made with the key counts, including errors, status polls and downloads. That 429 has code RATE_LIMITED, a Retry-After header, and in the body retryAfter (the same seconds), limit (60) and resetsAt (when the window resets): wait Retry-After seconds, then retry. Counts are kept per server instance, so always honor Retry-After rather than X-RateLimit-Remaining. List Builder limits carry their own codes: PREVIEW_MINUTE_LIMIT (limit, Retry-After: 60), PREVIEW_DAILY_LIMIT (limit, resetsAt and Retry-After), ACTIVE_PULL_LIMIT (limit), DOWNLOAD_LIMIT (limit), SKIP_TRACE_IN_PROGRESS and SKIP_TRACE_BUSY (Retry-After: 10). When Retry-After is sent, wait that many seconds; when it isn't, wait a few seconds and back off exponentially. The X-RateLimit-* headers on any 429 describe the per-key limit only. |
500 | Something failed on our side. Deep Search and Standard Search refund the credits they took (error ends with "Credits have been refunded."), and a search 500 also covers a temporary outage of the search service, so retry it after a short wait with exponential backoff. After a 500 from an upload or POST /lists, check reservedCredits on GET /credits (or GET /lists) for a job or list that was created before you retry. |
503 | Temporarily unavailable, for example code PREVIEW_UNAVAILABLE when a match count can't be fetched. Please try again. |
429 is the one worth designing for: the limit is 60
requests per minute per key across every endpoint, counted in a fixed
60-second window that starts with the key's first request, so a
backfill should either use the bulk endpoints or pace itself. Over the
limit, the 429 has code RATE_LIMITED and a Retry-After
header with the whole seconds until the window resets; the body repeats
it as retryAfter, with limit and
resetsAt, and the error message says how long to wait.
Wait that many seconds, then retry.
Every response to a request whose key is accepted, errors included,
carries X-RateLimit-Limit (60),
X-RateLimit-Remaining (requests left in this window) and
X-RateLimit-Reset (when the window resets, as a Unix
timestamp in seconds). Counts are kept per server instance, so treat
X-RateLimit-Remaining as a guide and always honor
Retry-After.
Which endpoint should you use?
- One record, need a phone or email. Standard Search.
Send
dataTypeswith the data you want; you are charged one credit per data type, and only on a match. This is the pattern behind phone number lookup and contact enrichment. - One record, the subject is unreachable. Deep Search. It returns the related-persons graph — spouses, relatives, associates — which is the path forward when the owner's own number is dead. See relational mapping.
- A whole list. Upload the CSV to the bulk endpoint,
poll status, then download results with
layout=standard. Each row needs a first and last name plus a ZIP or a city and state, or a full street address with city, state and ZIP. Full workflow on batch skip tracing. - Building the list itself. List Builder: preview a
filter set for free, create the list (1 credit per record pulled),
then read or download its records, or skip trace it with
POST /lists/{listId}/skip-trace. - Turning a list into ad audiences. Digital Matching returns ready-to-upload Custom Audience files per platform, up to 50,000 rows per upload. Ava Data prepares the files; you upload them to the ad platform yourself.
New to the underlying concept? Start with what skip tracing is, or read the skip tracing API overview for the commercial picture rather than the reference.
Download the spec
The machine-readable OpenAPI 3.0 document is at
/openapi.json (version 1.2.2),
and browser-based tools can import it by URL. Point your client
generator at it to produce a typed SDK rather than hand-writing
request code. The app serves the API's own copy at
https://app.avadata.ai/api/v1/openapi.json, as a file download with no CORS
headers; this site's copy adds a short publishing and pricing note and
longer introductions to the Search and Bulk groups.
Code samples on GitHub: avadata-ai/skip-tracing-api.