Intent Audiences API

Overview

The Audiences API lets you create targeted audiences based on intent signals. You define which topics you care about, and the platform finds contacts actively researching those topics.

Flow: find topics with the Taxonomy API, create an audience, then poll its status. While it is Pending or Syncing, keep polling. When it is Completed, preview a sample, then download the full results. Audience status lifecycle: creating an audience sets it to Pending. The system picks up the job and moves it to Syncing. Syncing ends in Completed when results are ready, then you download results, or in Failed when an error occurs, then you check the error and retry.

Base URL: https://api.delivr.ai

Prerequisites:

Authentication: Organization API key, or a project-scoped key for its own project. Send X-Api-Key + X-Api-Secret on every request (both required). Create the pair in the dashboard at https://app.delivr.ai/{org_id}/settings/api-keys.

Project scoping: All requests require your project ID, passed via either:

  • X-Project-Id header, OR
  • ?project_id= query parameter

With a project-scoped key, the project must be the key's own. Any other project returns 403.

X-Api-Key: YOUR_API_KEY
X-Api-Secret: YOUR_API_SECRET
X-Project-Id: YOUR_PROJECT_ID
Content-Type: application/json

Step 0: Get Available Topics

Before creating an audience, you need topic IDs. Browse the catalog using the Taxonomy API.

Quick Example

curl "https://api.delivr.ai/api/v1/taxonomy/topics?limit=10&search=cloud+computing" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET"

Example Response (200 OK)

{
  "response": {
    "topics": [
      {
        "topic_id": "4eyes_119418",
        "category_name": "Computers & Electronics",
        "subcategory_name": "Networking",
        "name": "Cloud Computing",
        "status": "active",
        "topic_type": "B2B"
      }
    ]
  }
}

Use the topic_id values (e.g. "4eyes_119418") in your audience filter's INTENT rule. See the Taxonomy API for filtering by category, subcategory, and type.

Step 1: Create an Audience

Request

POST /api/v1/audiences?project_id=YOUR_PROJECT_ID
{
  "organization_id": "YOUR_ORGANIZATION_ID",
  "project_id": "YOUR_PROJECT_ID",
  "audience_name": "Cloud Data Intent",
  "type": "intents",
  "segmentation_type": "Audience",
  "filter": {
    "condition": "and",
    "rules": [
      {
        "fieldName": "INTENT",
        "conditionRules": {
          "operator": "in",
          "value": ["4eyes_119418", "4eyes_516767"]
        }
      }
    ]
  }
}

Required Fields

FieldTypeDescription
organization_idstring (UUID)Your organization ID (provided during onboarding)
project_idstring (UUID)Your project ID (provided during onboarding)
audience_namestringA name for your audience
typestring"intents" for this example. Also accepts "contact" to build against the firmographic/contact database instead of intent data. See Understanding Segmentation Types for when a contact audience is the right choice. Note that its filter requirement differs (below).
segmentation_typestringMust be one of: "Audience", "Persona", "Account". See Understanding Segmentation Types for when to use each.
filterobjectMust contain at least one rule with fieldName: "INTENT" when type is "intents". For a "contact" audience, filter holds firmographic (company_*) or person fields instead, with no INTENT rule required.

Optional Fields

FieldTypeDefaultDescription
audience_descriptionstring""Description of the audience
selectstring"*"Which columns to include in results
unloadstring"preview""preview" for sample only, "unload" for full export
intent_window_daysinteger1Number of recent days of intent data to include (1-14). A wider window captures more contacts but may include older signals.
record_limitinteger(none)Maximum number of records to return. Omit for no limit.

unload defaults to preview. A preview build computes size and stats plus a small capped sample. It does not write the full downloadable dataset. To download the whole audience, set "unload": "unload" on create. (You can also drive the download endpoint, which prepares the full files on demand.)

Example Response (201 Created)

{
  "id": 8268,
  "organization_id": "YOUR_ORGANIZATION_ID",
  "project_id": "YOUR_PROJECT_ID",
  "audience_name": "Cloud Data Intent",
  "audience_description": "",
  "segmentation_type": "Audience",
  "status": "Pending",
  "type": "intents",
  "unload": "preview",
  "filter": { "..." },
  "sql_filter_statement": {
    "where": "topic_id in ('4eyes_119418', '4eyes_516767')"
  },
  "select": "*",
  "size": null,
  "date_updated_size": null,
  "output_path": null,
  "date_updated_path": null,
  "error": null,
  "task_id": null,
  "created_at": "2026-02-01T10:00:00Z",
  "updated_at": "2026-02-01T10:00:00Z"
}

Save the id from the response -- you need it for all subsequent requests.

Validation Rules

  • type: "intents" requires at least one rule where fieldName is INTENT, intent, topic, or topic_id
  • Without this, you get: "topic_id or INTENT field with topic IDs is mandatory for intents audience"
  • project_id in the body must match the project ID from your auth context

Step 2: Check Audience Status

After creating, the platform processes your audience in the background, so poll GET /api/v1/audiences/{id} until status reaches a terminal value. Treat Completed (or the legacy Validated) as success and Failed as error. Treat every other value (Pending, Syncing, anything unrecognized) as still in progress and keep polling. Builds usually finish in well under two minutes but can run several minutes for large audiences. For that reason, do not hard-code a short timeout that fails the job. Instead, poll until terminal (allow at least ~20 minutes) and requeue rather than fail permanently if you hit your own ceiling.

Request

GET /api/v1/audiences/{id}?project_id=YOUR_PROJECT_ID

Example Response (200 OK)

{
  "id": 4769,
  "organization_id": "YOUR_ORGANIZATION_ID",
  "project_id": "YOUR_PROJECT_ID",
  "audience_name": "Cloud Data Intent",
  "audience_description": "",
  "segmentation_type": "Audience",
  "status": "Completed",
  "type": "intents",
  "unload": "preview",
  "filter": { "..." },
  "sql_filter_statement": { "..." },
  "select": "*",
  "size": 1000,
  "date_updated_size": "2026-01-31T09:19:44.934716Z",
  "output_path": "s3://...",
  "date_updated_path": "2026-01-31T09:19:44.934716Z",
  "error": null,
  "task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "created_at": "2026-01-12T12:09:49.187774Z",
  "updated_at": "2026-01-31T09:19:44.937174Z"
}

Status Values

StatusMeaningWhat To Do
PendingQueued for processingPoll again in 10 seconds
SyncingProcessing in progressPoll again in 10 seconds
CompletedAudience is readyProceed to sample or download
ValidatedLegacy terminal success, equivalent to CompletedProceed to sample or download
FailedAn error occurredCheck the error field
PausedAuto-renewal is stopped. Existing results are still downloadableResume if you need fresh data

An intent audience typically builds in 30-120 seconds, depending on its size. Poll status to tell when a build has finished, not the counts: a completed build can leave distinct_profile_count null until it is first downloaded or paged.

Step 3: Preview Sample Data

Once status is "Completed", preview a sample of the results.

Request

GET /api/v1/audiences/{id}/sample?limit=5&project_id=YOUR_PROJECT_ID

Query Parameters

ParameterTypeDefaultDescription
limitinteger25Number of rows to return (capped server-side at 25)
project_idstringNoneYour project ID

Example Response (200 OK)

{
  "total_count": 27593,
  "last_processed_at": "2026-02-10T00:00:00Z",
  "masked": true,
  "sample_masked": true,
  "field_coverage": {
    "business_email": 1,
    "personal_email": 1,
    "mobile_phone": 1,
    "direct_phone": 0,
    "linkedin_url": 1,
    "job_title": 1,
    "seniority": 1,
    "department": 1,
    "company_name": 1,
    "company_domain": 1,
    "postal_address": 0
  },
  "rows": [
    {
      "department": "operations",
      "seniority_band": "Staff",
      "company_name": "acme corp",
      "company_domain": "acmecorp.com",
      "company_industry": "insurance",
      "company_employee_count": 2400,
      "company_city": "new york",
      "company_state": "new york",
      "company_country": "united states",
      "has_business_email": true,
      "has_personal_email": true,
      "has_phone": true,
      "has_linkedin": true,
      "score": "high",
      "topic_id": "4eyes_110486",
      "topic_name": "15Five",
      "ts": "2026-01-30T00:00:00"
    }
  ]
}
FieldTypeDescription
total_countintegerTotal number of records matching the audience filters. Use this to gauge audience size before downloading.
last_processed_atstring (ISO 8601)Timestamp of when the audience data was last processed.
rowsarrayMasked sample rows (up to 25). See below for field details.
maskedbooleanAlways true on an API key.
sample_maskedbooleanAlways true on a sample.
field_coverageobjectFor each field the download returns, how many sampled people hold it.

total_count and last_processed_at are present when the audience has completed processing. For audiences still in Pending or Syncing status, these fields are omitted.

The sample is a capped, redacted teaser for validating the audience, not a data feed. Each row carries:

  • Role: department and seniority_band (VP+, Director, Manager or Staff)
  • Company info: name, domain, industry, size, revenue, location
  • Intent fields (added by processing): score, perc_score, topic_id, topic_name, intent_frequency, ts
  • Presence flags: has_business_email, has_personal_email, has_phone, has_linkedin. These indicate which contact fields exist for the row.

Names, titles, contact data (emails, phones, LinkedIn URLs, personal addresses) and the hashed email are not returned by the sample. Download the audience (Step 4) to get, and be billed for, the full contact records. See Intent Audience Fields for the complete export schema.

Note: The sample endpoint returns up to 25 rows. A limit above 25 is clamped to 25.

Step 4: Download the Full Audience

The download endpoint prepares files and returns signed URLs. If files are not yet prepared, it automatically triggers preparation and returns a status you can poll.

Request

GET /api/v1/audiences/{id}/download?project_id=YOUR_PROJECT_ID

Preview the cost first (optional)

Add dry_run=true to see what a download would bill before you pull anything. It returns the billing meter and the records-to-charge for the audience, triggers no file preparation, and bills nothing:

GET /api/v1/audiences/{id}/download?project_id=YOUR_PROJECT_ID&dry_run=true

If the audience uses partner data fields, the response also carries partner_charges, with one line per dataset. A live download adds these lines on top of the base charge.

When a download is billed again

A build is billed once. Re-downloading that same build is never billed again. There is no time limit on this: not monthly, not per contract period, not per audience lifetime. Retries, re-signed links, and downloading the same build from more than one place all settle to the single original charge.

What ends it is a rebuild. The billing key is the audience id, the filter_fingerprint, the effective record_limit, and the data window (data_start_date to data_end_date) the build actually covered. A rebuild that covers newer intent data moves the window, so the next download of it is a new charge.

So for a recurring audience that rebuilds daily and is downloaded after each rebuild, expect roughly one charge per day. The exception is a day when upstream intent data has not advanced. On that day the audience covers the same window, so the download settles to the existing charge and costs nothing.

A rebuild itself bills nothing. The next download bills the whole audience, not the newcomers. The quantity is distinct people in the rebuilt audience, capped by record_limit when you have set one that binds. It is not the difference from the previous build, and it is not zero.

Two things that do not create a charge:

  • Editing the filter changes filter_fingerprint, but that alone bills nothing. It only stops an edited audience from settling against an earlier, different charge.
  • Setting or removing a record_limit at or above size, the row count of the most recent build. In that count, a person appears once per email and, on person_topic grain, once per topic. At or above size, a limit is treated as no limit at all, both in the billing key and in the quantity.

One trap in that second one: the threshold is size, not distinct_profile_count, and size is the larger of the two. A record_limit set between them does not change what you are billed, because the quantity is capped on distinct people. It does change the billing key. That download will not settle against the earlier charge. If you want a rebuild to settle, leave record_limit alone.

If you want to reduce what you are billed, the lever is rebuild cadence, not filter stability. An audience set to rebuild weekly is billed roughly weekly. Use dry_run=true above to see the quantity for any given build before pulling it. To pay only for people you have not received this month, see Download only people new this month.

Download only people new this month

If you pull an audience on a schedule, call /download/new instead of /download. It leaves out people already delivered to the audience's project this UTC calendar month at the audience's people meter. Each person is billed once per month.

GET /api/v1/audiences/{id}/download/new?project_id=YOUR_PROJECT_ID

Poll it as you poll /download, until status is ready. When it is ready, output_links holds only the new people, and new_only reports new_people, already_delivered and billed_people. Polling or downloading the same build again in the same month does not bill twice.

  • dry_run=true returns an upper bound: the audience's people. The actual charge is at most that.
  • page returns 400.
  • An audience with partner data, or one that delivers intent or add-on columns, returns 409. Use /download for it, which bills every delivery.

Response When Ready (200 OK)

{
  "status": "ready",
  "stage": "ready",
  "output_links": {
    "day=20260130/prefix=4eyes_110/score=high": [
      "https://signed-s3-url/count.json?...",
      "https://signed-s3-url/part-0.parquet?..."
    ],
    "day=20260130/prefix=4eyes_500/score=high": [
      "https://signed-s3-url/count.json?...",
      "https://signed-s3-url/part-0.parquet?..."
    ]
  }
}

The output_links keys are partitioned by day, topic prefix, and score. Each partition contains:

  • count.json -- record count for that partition
  • preview.json -- preview data for that partition
  • One or more .parquet data files with the actual audience data (e.g. part-0.parquet, part-1.parquet)

Some partitions may only have count.json (when no matching data was found for that topic/score combination).

Signed URLs are presigned for up to 7 days. Download promptly, and if a link ever returns an expired-signature error, request the download endpoint again for freshly signed URLs.

Counts On A Capped Download

record_count is the rows the build produced, before record_limit. A capped download states the rows you actually received in delivered_count:

{
  "status": "ready",
  "format": "csv",
  "record_limit": 500,
  "record_count": 2019914,
  "delivered_count": 500,
  "output_links": { "csv": ["https://signed-s3-url/audience.csv?..."] }
}

delivered_count is present on format=csv downloads. It is absent on parquet downloads and on CSVs prepared before the field existed. Treat absent as unknown, not zero. GET /api/v1/audiences/{id}/records reports the same build total in count and pages through the build output with next_cursor. record_limit does not apply there, so delivered_count is not that endpoint's total.

Response When The Audience Matched No Records (200 OK)

{
  "status": "ready",
  "stage": "ready",
  "record_count": 0,
  "output_links": {}
}

A build that matched nobody is finished, not failed and not still working. There is no file and there never will be one, so a client that keeps polling this is waiting for something that cannot arrive. Widen the audience definition to get records.

record_count is the signal, and only a stated zero means empty. The field is absent when the build reported no count, which is a different thing. Treat absent as unknown and fall back to what output_links gives you. Test for the value rather than for falsiness, or the two collapse into one case.

output_links is present on every download response. It is an empty object here rather than a missing key, so the same iteration works whether or not there is anything to iterate.

Response When Not Ready (200 OK)

{
  "status": "unloading",
  "reason": "full_download_requested",
  "stage": "deduplicating",
  "progress": {
    "completed": 18,
    "total": 72
  }
}

While preparation is in flight, stage is one of waiting, building, deduplicating, or finalizing. The ready response carries stage: "ready", so handle five values in total, and treat an unrecognized one as "still preparing". When the active worker reports job counts, progress contains the completed and total jobs. Both fields may be absent, so clients should still treat status as the source of truth.

Download Status Values

The typical progression is: unloading -> done -> ready.

StatusMeaningWhat To Do
readyFinished. Files are in output_links, or record_count is 0 and there are noneDownload the files, or report an empty audience
unloadingFiles are being preparedPoll again in 3 seconds
donePreparation finishingPoll again in 3 seconds
failedPreparation failedRetry or contact support

These values are not the values of GET /api/v1/audiences/{id}/status. Here, done means the files are still being prepared. A done from GET /api/v1/audiences/{id}/status means the build finished.

Keep polling until the endpoint returns ready or failed. Every other status is non-terminal, including converting, no_task, in_progress, completed, and unrecognized future values. Large audiences can take longer than a few minutes, so a client-side timeout must not turn healthy work into an error. A UI may stop making requests when the user closes it, but preparation continues on the server. Resume polling when the user returns.

Downloading the Files

Files are in Parquet format. See Reading Parquet Files for how to convert them to CSV or open in Excel.

Filter the URLs to find .parquet files (skip count.json unless you need record counts):

import requests

response = requests.get(
    "https://api.delivr.ai/api/v1/audiences/AUDIENCE_ID/download",
    params={"project_id": "YOUR_PROJECT_ID"},
    headers={"X-Api-Key": "YOUR_API_KEY", "X-Api-Secret": "YOUR_API_SECRET"},
)
data = response.json()

if data["status"] == "ready":
    # A stated zero is a finished build that matched nobody. Absent means no
    # count was reported, which is not the same thing, so compare the value
    # instead of testing for falsiness.
    if data.get("record_count") == 0:
        print("This audience matched no records. Widen its definition to get some.")
    for partition, urls in data["output_links"].items():
        for url in urls:
            if url.split("?")[0].endswith(".parquet"):
                # Download parquet file
                r = requests.get(url)
                filename = partition.replace("/", "_") + ".parquet"
                with open(filename, "wb") as f:
                    f.write(r.content)

Available Fields (Schema)

To see all available fields and their data types:

GET /api/v1/audiences/schema?project_id=YOUR_PROJECT_ID

Example Response (200 OK)

{
  "fields": [
    { "name": "address_id", "data_type": "Utf8View", "nullable": true },
    { "name": "age_range", "data_type": "Utf8View", "nullable": true },
    { "name": "business_emails", "data_type": "Utf8View", "nullable": true },
    { "name": "company_address", "data_type": "Utf8View", "nullable": true },
    { "name": "company_city", "data_type": "Utf8View", "nullable": true },
    { "name": "company_country", "data_type": "Utf8View", "nullable": true },
    { "name": "company_domain", "data_type": "Utf8View", "nullable": true },
    { "name": "company_name", "data_type": "Utf8View", "nullable": true },
    { "name": "company_size", "data_type": "Utf8View", "nullable": true },
    { "name": "first_name", "data_type": "Utf8View", "nullable": true },
    { "name": "job_title", "data_type": "Utf8View", "nullable": true },
    { "name": "last_name", "data_type": "Utf8View", "nullable": true },
    { "name": "score", "data_type": "Utf8View", "nullable": true },
    { "name": "topic_id", "data_type": "Utf8View", "nullable": true },
    { "name": "topic_name", "data_type": "Utf8View", "nullable": true }
  ]
}

Use the name values from this response as fieldName in your filter rules.

Note: This is a partial list. The full schema contains additional fields. Call the endpoint to see all available fields.

Filter Reference

The filter field is a tree of rules joined by "and" or "or".

Structure

{
  "condition": "and",
  "rules": [ ... ]
}

Topics (required for intents)

{
  "fieldName": "INTENT",
  "conditionRules": {
    "operator": "in",
    "value": ["topic_id_1", "topic_id_2"]
  }
}

Get topic IDs from GET /api/v1/taxonomy/topics (Step 0). The dashboard's audience builder caps selection at 10 topics. The create endpoint does not enforce that cap, so a filter naming more is accepted. The limit is a builder decision rather than a constraint of the API.

Signal Strength (optional)

If omitted, all strengths are included.

{
  "fieldName": "score",
  "conditionRules": {
    "operator": "in",
    "value": ["high", "medium"]
  }
}
ValueMeaning
highStrong intent signal
mediumModerate intent signal
lowWeak intent signal (broadest reach)

Field Filters (optional)

Use any field name from the schema endpoint:

{
  "fieldName": "company_employee_count_range",
  "conditionRules": {
    "operator": "in",
    "value": ["1001 to 5000", "5001 to 10000", "10000+"]
  }
}

Large value sets: To filter by more than a few dozen values (e.g., thousands of email hashes or domains), upload them as a list. Do not pass them as an array in the filter. Large arrays in the filter body slow down audience creation. A filter holds at most 50,000 inline values across its in, not in, include and exclude rules, and a request body over 8 MiB is refused with 413 (request_too_large). Create, update and estimate return 400 past the value limit. See Using Lists with Audiences below.

Available Operators

OperatorValue TypeDescription
inarray of stringsMatches any of the values
not inarray of stringsExcludes these values
is / equalsstring or arrayField exactly equals value
not equalsstringField does not equal value
containsstring or arrayField contains the text, even inside a word
containsWordstring or arrayField holds the value as a whole word. A list matches any of its words
notcontainsstringField does not contain the text. One value per rule
notContainsWordstring or arrayField holds none of the words as a whole word. Keeps rows with no value
startsWithstringField starts with the text
endsWithstringField ends with the text
is not blank(none)Field has a value
is blank(none)Field is empty or null
is not null / notnull(none)Field is not null
is null / isnull(none)Field is null
>=stringGreater than or equal to
<=stringLess than or equal to
>stringGreater than
<stringLess than

Values can be provided as:

  • A plain string: "value": "some text"
  • An array of strings: "value": ["val1", "val2"]
  • An array of objects: "value": [{"name": "val1"}, {"name": "val2"}] (backend extracts the name, value, or label field)

Field Types and Operators

Fields fall into two categories. Using the wrong operator type is the most common cause of unexpectedly small audiences.

Picklist Fields

Picklist fields have a fixed set of valid values. Use the operators shown for each field.

FieldOperatorsExample Values
seniority_levelin, notincxo, director, manager, staff, vp
departmentin, notinsales, marketing, engineering, finance, operations, executive, information technology, human resources, legal, administrative, customer service, product management, education, healthcare services, media and communication, community and social services (16 values total)
job_title_normalizedin, notin, contains16,000+ normalized titles. Use GET /api/v1/field-catalog/job_title_normalized to browse values. in for exact matching, contains for partial matching.
company_industryin, notintechnology, information and internet, software development, financial services, advertising services, hospitals and health care, insurance, ... (200+ values, call the field catalog endpoint for the full list)
company_revenue_rangein, notinunder 1 million, 1 million to 5 million, 5 million to 10 million, 10 million to 25 million, 25 million to 50 million, 50 million to 100 million, 100 million to 250 million, 250 million to 500 million, 500 million to 1 billion, 1 billion and over
company_employee_count_rangein, notinzero, 1 to 10, 11 to 25, 26 to 50, 51 to 100, 101 to 250, 251 to 500, 501 to 1000, 1001 to 5000, 5001 to 10000, 10000+
age_rangein, notin, is, is not18-24, 25-34, 35-44, 45-54, 55-64, 65 and older
genderin, notin, is, is notf, m, u
income_range_lcin, notinless than $20,000, $20,000 to $44,999, $45,000 to $59,999, $60,000 to $74,999, $75,000 to $99,999, $100,000 to $149,999, $150,000 to $199,999, $200,000 to $249,000, $250,000+
email_validation_statusin, notinvalid, invalid, catchall, unknown

Tip: Use GET /api/v1/field-catalog/{field_key} to get the full list of valid values for any picklist field. See the Field Catalog API for details.

Free-Text Fields

Free-text fields contain arbitrary strings (names, titles, addresses). Supported operators: is, is not, contains, notcontains, startsWith, endsWith, notnull. Title, department and company name fields also take containsWord and notContainsWord. Word operators work on text fields only. See Building Audience Filters for when to use each one.

FieldRecommended OperatorExample
job_titlecontains"value": "director" matches titles that contain "director", for example "Director of Sales" and "Marketing Director".
job_title (short words such as "cto")containsWord"value": "cto" matches "CTO, Acme" and does not match "Director".
first_name, last_nameis or contains"value": "jane"
company_namecontains"value": "acme"
company_domainis or contains"value": "acme.com"
current_business_emailnotnull(no value needed)
personal_city, personal_stateis or contains"value": "new york"
company_city, company_stateis or contains"value": "san francisco"

job_title vs job_title_normalized: Both fields support contains for partial matching. job_title is raw free-text (e.g., "Sr. Director of Engineering"). job_title_normalized is a standardized picklist (16,000+ values) that also supports in/notin for exact matching. Use job_title with contains for broad matching on raw titles. Use job_title_normalized with in for precise matching against known normalized values. Use it with contains for partial matching against normalized titles.

Matching Multiple Titles

Short acronyms (CTO, CEO, CFO) have no exact-match value. job_title_normalized holds full titles rather than acronyms, and most C-level roles have no plain entry in it. There is no chief technology officer, chief executive officer or chief financial officer. Only compound titles such as chief financial officer and vice president exist. chief operating officer and vice president are among the few that exist. Look a value up with GET /api/v1/field-catalog/job_title_normalized before filtering on it with in.

For a level rather than a role, use seniority_level: cxo for C-level people, vp for vice presidents.

Warning: Do not use contains with short acronyms on job_title. Substring matching means "CTO" matches "Art Director" and "VP" matches "Advpush". Spell the title out, or use seniority_level.

{
  "fieldName": "seniority_level",
  "conditionRules": {
    "operator": "in",
    "value": ["cxo", "vp"]
  }
}

For a specific role, spell the title out and use contains on job_title, with an or group for several. A phrase like chief technology officer cannot occur inside an unrelated word:

{
  "condition": "or",
  "rules": [
    {
      "fieldName": "job_title",
      "conditionRules": { "operator": "contains", "value": "Director of Engineering" }
    },
    {
      "fieldName": "job_title",
      "conditionRules": { "operator": "contains", "value": "Head of Product" }
    }
  ]
}

Listing Audiences

Request

GET /api/v1/audiences?project_id=YOUR_PROJECT_ID&type=intents&page=1&page_size=20

Query Parameters

ParameterTypeDefaultDescription
project_idstringNoneYour project ID (required)
typestringNoneFilter by type: intents, contact, list, pixel. elixir is the former name for contact and is still accepted. Either value returns every contact audience
statusstringNoneFilter by status: Pending, Syncing, Completed, Failed or Paused
pageinteger1Page number
page_sizeinteger20Results per page, up to 100
sort_bystringNoneSort column: audience_name, status, created_at
sort_orderstringNoneASC or DESC
namestringNoneSearch by audience name

Example Response (200 OK)

{
  "data": [
    {
      "id": 4879,
      "organization_id": "YOUR_ORGANIZATION_ID",
      "project_id": "YOUR_PROJECT_ID",
      "audience_name": "Automated Customer Service Need",
      "audience_description": "Business owners interested in automating customer service",
      "segmentation_type": "Audience",
      "status": "Completed",
      "type": "intents",
      "size": 4899,
      "created_at": "2026-01-28T18:52:02.850618Z",
      "updated_at": "2026-02-01T00:02:35.076531Z"
    }
  ],
  "page": 1,
  "page_size": 20,
  "total": 15,
  "total_pages": 1
}

Pagination

The response includes pagination metadata:

FieldDescription
dataArray of audience objects
pageCurrent page number
page_sizeNumber of results per page
totalTotal number of matching audiences
total_pagesTotal number of pages

Complete Example

1. Create

curl -X POST "https://api.delivr.ai/api/v1/audiences?project_id=YOUR_PROJECT_ID" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "organization_id": "YOUR_ORGANIZATION_ID",
    "project_id": "YOUR_PROJECT_ID",
    "audience_name": "Enterprise Cloud Computing Intent",
    "audience_description": "Large companies researching cloud infrastructure",
    "type": "intents",
    "segmentation_type": "Audience",
    "filter": {
      "condition": "and",
      "rules": [
        {
          "fieldName": "INTENT",
          "conditionRules": {
            "operator": "in",
            "value": ["4eyes_119418", "4eyes_118710"]
          }
        },
        {
          "fieldName": "score",
          "conditionRules": {
            "operator": "in",
            "value": ["high", "medium"]
          }
        },
        {
          "fieldName": "company_employee_count_range",
          "conditionRules": {
            "operator": "in",
            "value": ["1001 to 5000", "5001 to 10000", "10000+"]
          }
        }
      ]
    }
  }'

2. Poll Status (repeat until "Completed")

curl "https://api.delivr.ai/api/v1/audiences/AUDIENCE_ID?project_id=YOUR_PROJECT_ID" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET"

3. Preview Sample

curl "https://api.delivr.ai/api/v1/audiences/AUDIENCE_ID/sample?limit=5&project_id=YOUR_PROJECT_ID" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET"

4. Download

# Poll until status is "ready"
curl "https://api.delivr.ai/api/v1/audiences/AUDIENCE_ID/download?project_id=YOUR_PROJECT_ID" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET"

# Download parquet files from the signed URLs in output_links
curl -o audience_data.parquet "SIGNED_URL_FROM_RESPONSE"

Other Operations

ActionMethodEndpointResponse
UpdatePUT/api/v1/audiences/{id}?project_id=...200 with updated audience
PausePOST/api/v1/audiences/{id}/pause?project_id=...200 with updated audience
ResumePOST/api/v1/audiences/{id}/resume?project_id=...200 with updated audience
RefreshPOST/api/v1/audiences/{id}/refresh?project_id=...202 with updated audience
ClonePOST/api/v1/audiences/{id}/clone?project_id=...201 with new audience
DeleteDELETE/api/v1/audiences/{id}?project_id=...204 No Content

The Update endpoint (PUT) does not accept a status field. To stop or resume auto-renewal, use the dedicated pause and resume actions below. Pause/resume preserve the audience's schedule (stashed in previous_renew_time) and surface the state via paused_at + pause_reason on the audience response.

Pause an Audience

Stop the audience from auto-renewing on its scheduled renew_time. The audience's results, filter, and stored schedule are preserved. Only the renewal loop stops picking it up.

POST /api/v1/audiences/{id}/pause?project_id=YOUR_PROJECT_ID

Response (200 OK) is the updated audience with paused_at set to the current time and pause_reason = "manual". Returns 409 Conflict if the audience is already paused.

Refresh an Audience

Re-run the audience against current data without recreating it. This resets the audience to Pending so the worker rebuilds it on the next pass.

POST /api/v1/audiences/{id}/refresh?project_id=YOUR_PROJECT_ID

Response (202 Accepted) is the updated audience, now back in the Pending status. Poll status as you would after a create.

Use this rather than PUT when the filter has not changed. An update that submits an identical filter is treated as a no-op and will not trigger a rebuild, so it looks like nothing happened. refresh always re-runs.

StatusWhen
400The audience type does not support background refresh.
404No audience with that id in the project.
409The audience is already processing (Syncing), or it is paused. A paused audience returns a pause_reason. Resume the audience first.

Resume an Audience

Clear the pause and restore the previously stashed renewal schedule.

POST /api/v1/audiences/{id}/resume?project_id=YOUR_PROJECT_ID

Response (200 OK) is the updated audience with paused_at and pause_reason cleared and renew_time restored from previous_renew_time.

Returns 409 Conflict when the audience is not paused, or when it is paused for a non-manual reason. In those cases the body includes a machine-readable code:

codeMeaning
org_inactiveThe parent organization is inactive. Reactivate it before resuming.
project_inactiveThe parent project is inactive. Reactivate it before resuming.
(absent)The audience is not currently paused.

Errors

All error responses return a JSON object with an error field:

HTTP StatusExample error Message
400"project_id is required"
400"topic_id or INTENT field with topic IDs is mandatory for intents audience"
400"failed to transform filter: ..."
401"invalid or expired token: ..."
403"Partner data fields are not available on this account yet." (a filter or select uses a partner data dataset that is not enabled for the project)
404"Audience not found"
409"partner_price_not_accepted" or "partner_dataset_disabled" on create, update or download (see Partner data)
500"Failed to retrieve audience"

Note: project_id is required on all endpoints. Omitting it returns a 400 error.

Check File Status

Check whether an audience's output files are ready for download without triggering file preparation. The status endpoint is a lightweight alternative to the download endpoint when you only need to know if files exist.

Request

GET /api/v1/audiences/{id}/status?project_id=YOUR_PROJECT_ID

Example Response (200 OK)

{
  "status": "ready"
}

Status Values

StatusMeaningWhat To Do
readyThe build finished and its download URLs are validPage the records, or call the download endpoint
doneThe build finished and its download URLs expiredPage the records, or call the download endpoint for new URLs
unloadingThe build is runningPoll again in 3-5 seconds
countedOnly the audience size was builtCall POST /api/v1/audiences/{id}/unload, then poll again
no_taskThe audience has no buildCall POST /api/v1/audiences/{id}/unload, then poll again
failedThe build failedCall POST /api/v1/audiences/{id}/unload to rebuild, then poll again
unknownThe build state could not be readPoll again in 3-5 seconds

POST /api/v1/audiences/{id}/unload bills nothing. Do not call the download endpoint to start a build: a ready download bills every distinct person in the build.

When to Use

Use the status endpoint instead of the download endpoint when you want to check file readiness without side effects. The download endpoint (GET /api/v1/audiences/{id}/download) triggers file preparation if files do not exist yet. The status endpoint only checks. It never triggers preparation.

The status endpoint provides coarse readiness only. After a user requests a download, poll the download endpoint when the UI needs the normalized stage and optional job progress. Do not impose a fixed client-side timeout. Stop polling only when the response is ready or failed. You can also pause requests while the UI is closed and resume them when it reopens.

The status endpoint is useful for:

  • Showing a progress indicator in the UI after triggering a download
  • Polling for readiness before enabling a "Download" button
  • Checking if a previously exported audience still has valid files

Using Lists with Audiences

Lists let you upload your own data (hashed emails, domains, phone numbers) and use it to include or exclude contacts.

There are two places a list can be applied, and they are not interchangeable:

WhereWhat it doesWorks with
On the audienceThe list filters the audience itself, so every export and campaign built from it inherits the filter.Any list type, matched on the field you choose
On a pixel exportThe list filters one export's output, via audienceFilters.hem lists only, and sourceType: "pixel" only

Suppressing a set of companies or people from an audience is the first one. Upload the list, then add it to the audience as an exclusion in the audience builder. Open the audience, add the list, choose the field to match on, and set it to exclude. Everything the audience feeds is filtered too. A list of company domains works here, which the export filter cannot do.

The builder writes the list into the audience's stored filter for you, resolving your list to the location the build reads. That resolution is why this is a dashboard step rather than a hand-written filter payload. The stored rule references the list's materialized location, not its id, and there is no public endpoint that returns it. Create and update audiences through the API by all means. Add the list exclusion in the builder.

An exclusion on an audience always applies. Included lists can be combined with OR against a single match field, but exclusions never OR with anything: every one of them is applied.

Base URL: https://api.delivr.ai

List Types

TypeDescriptionExample Values
hemSHA256-hashed emailse3b0c44298fc1c149afbf4c8996fb924...
contactContact identifiersEmail addresses
accountAccount identifiersCompany domains
domainDomain namesacme.com
zipcodeZip/postal codes90210
phonePhone numbers+15551234567
addressUS postal addresses, matched at household level235 S Jane St|Apt 202|Haysville|KS|67060

An address value is street|unit|city|state|zip with exactly four | separators. Leave a part empty rather than dropping it (235 S Jane St||Haysville|KS|67060). Street and ZIP are required. Matching uses the normalized street, unit and ZIP, so Street/St and Apartment/Apt/# spellings are treated alike. An audience built with the list reaches every person at each address. A unit on only one side does not match.

Quick Example: Upload a HEM Suppression List

1. Create the list

curl -X POST "https://api.delivr.ai/api/v1/lists" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "organization_id": "YOUR_ORG_ID",
    "project_id": "YOUR_PROJECT_ID",
    "name": "Suppression List Q2",
    "type": "hem",
    "source": "upload"
  }'

Save the id from the response.

2a. Add values (small lists, under 100k items)

curl -X POST "https://api.delivr.ai/api/v1/lists/LIST_ID/values/batch" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "values": [
      "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
      "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
    ]
  }'

Response:

{
  "inserted_count": 2,
  "skipped_count": 0,
  "total_count": 2
}

2b. Upload a file (large lists, up to 50 MB)

# Step 1: Get a presigned upload URL
curl -X POST "https://api.delivr.ai/api/v1/lists/LIST_ID/imports/presign" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"filename": "suppression.csv", "size_bytes": 5000000}'

# Step 2: Upload the file to S3 using the presigned URL and fields
curl -X POST "UPLOAD_URL_FROM_RESPONSE" \
  -F "Content-Type=text/csv" \
  -F "key=S3_KEY_FROM_FIELDS" \
  -F "policy=POLICY_FROM_FIELDS" \
  -F "x-amz-algorithm=ALGORITHM_FROM_FIELDS" \
  -F "x-amz-credential=CREDENTIAL_FROM_FIELDS" \
  -F "x-amz-date=DATE_FROM_FIELDS" \
  -F "x-amz-signature=SIGNATURE_FROM_FIELDS" \
  -F "[email protected]"

# Step 3: Trigger the import
curl -X POST "https://api.delivr.ai/api/v1/lists/LIST_ID/imports" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"s3_key": "S3_KEY_FROM_PRESIGN"}'

# Step 4: Poll until completed
curl "https://api.delivr.ai/api/v1/lists/imports/JOB_ID" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET"

The CSV file should have one value per line (with or without a header row). The import processes values in batches of 50,000 and reports progress via progress_pct (0-100).

Using Lists in Exports

Lists in exports are the second of the two places above, and they are narrower than filtering on the audience. Two limits apply, and both are rejections rather than silent no-ops:

  • Pixel exports only. audienceFilters is accepted when the export's sourceType is pixel. On any other source type the export is rejected with audience filters are only supported for pixel exports.
  • hem lists only. The filter matches on hashed emails, so the list must have been created with type: "hem". A list of another type is refused when the export runs, not when you save the export. A saved export can therefore fail on its first run.

To suppress companies by domain, or to filter anything that is not a pixel export, put the list on the audience instead.

Once uploaded, reference a list by its ID in an export's audienceFilters:

{
  "sourceType": "pixel",
  "sourceId": "YOUR_PIXEL_ID",
  "audienceFilters": [
    {
      "listId": "LIST_ID",
      "mode": "include",
      "hemFormat": "sha256_lc"
    }
  ]
}
FieldValuesDescription
listIdUUIDThe list ID from step 1
modeinclude or excludeInclude only matching contacts, or exclude them
hemFormatsha256_lc, sha256_uc, md5_lc, md5_uc, sha1_lc, sha1_ucHash format of the values in your list

Next Steps


Did this page help you?