Create an Intent Audience (End-to-End)
Browse topics, create audience, poll status, preview, and download
flowchart LR
A[Find Topics] --> B[Create Audience]
B --> C[Poll Until Complete]
C --> D[Preview Sample]
D --> E[Download Parquet]
style A fill:#3b82f6,color:#fff
style E fill:#22c55e,color:#fff
Use Case
- Create a targeted audience from intent topics and download the results
- End-to-end workflow: browse topics, create audience, poll status, preview, download
- Automate audience creation for recurring campaigns
Prerequisites
- Organization API key + secret pair (Authentication). Create one at
https://app.delivr.ai/{org_id}/settings/api-keys. - A project ID (Account Setup)
Steps
1. Find Topics
Browse the taxonomy to find topics relevant to your campaign. The Taxonomy API is on api.delivr.ai.
import requests
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
HEADERS = {
"X-Api-Key": API_KEY,
"X-Api-Secret": API_SECRET,
}
# Search for topics by name
resp = requests.get(
"https://api.delivr.ai/api/v1/taxonomy/topics",
headers=HEADERS,
params={"search": "cloud computing", "limit": 10},
)
topics = resp.json()["response"]["topics"]
for t in topics:
print(f" {t['topic_id']}: {t['name']} ({t['topic_type']})")Example output:
4eyes_119418: Cloud Computing (B2B)
4eyes_112199: Alibaba Cloud (B2B)
4eyes_118710: Amazon Elastic Compute Cloud (B2B)
Save the topic_id values you want to target.
2. Create the Audience
import requests
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
ORG_ID = "your_organization_id"
PROJECT_ID = "your_project_id"
HEADERS = {
"X-Api-Key": API_KEY,
"X-Api-Secret": API_SECRET,
"Content-Type": "application/json",
}
TOPIC_IDS = ["4eyes_119418", "4eyes_112199"]
resp = requests.post(
f"https://api.delivr.ai/api/v1/audiences?project_id={PROJECT_ID}",
headers=HEADERS,
json={
"organization_id": ORG_ID,
"project_id": PROJECT_ID,
"audience_name": "Cloud Computing Intent - Q1 2026",
"audience_description": "Contacts researching cloud computing topics",
"type": "intents",
"segmentation_type": "Audience",
"filter": {
"condition": "and",
"rules": [
{
"fieldName": "INTENT",
"conditionRules": {
"operator": "in",
"value": TOPIC_IDS,
},
},
{
"fieldName": "score",
"conditionRules": {
"operator": "in",
"value": ["high", "medium"],
},
},
],
},
},
)
audience = resp.json()
audience_id = audience["id"]
print(f"Created audience {audience_id}, status: {audience['status']}")Response:
{
"id": 8269,
"status": "Pending",
"audience_name": "Cloud Computing Intent - Q1 2026",
"type": "intents",
"segmentation_type": "Audience",
"...": "additional fields"
}3. Poll Until Complete
The platform processes your audience in the background. Poll GET /api/v1/audiences/{id} until the build reaches a terminal state.
Treat Completed (or the legacy Validated) as success and Failed as error. Treat every other value - Pending, Syncing, Running, anything unrecognized - as still in progress and keep polling. Keying off "is it terminal?" rather than matching one exact string keeps your loop working if a status is ever renamed.
import time
while True:
resp = requests.get(
f"https://api.delivr.ai/api/v1/audiences/{audience_id}",
headers=HEADERS,
params={"project_id": PROJECT_ID},
)
status_data = resp.json()
status = status_data["status"]
size = status_data.get("size")
print(f" Status: {status}, size: {size}")
if status in ("Completed", "Validated"):
print(f"Audience ready: {size} contacts")
break
elif status == "Failed":
print(f"Error: {status_data.get('error')}")
break
time.sleep(10)Status progression: Pending -> Syncing -> Completed
Typical processing time is 30-120 seconds depending on audience size. Do not hard-code a short client-side timeout that marks the order failed: builds can run several minutes for large audiences. Poll until terminal, allow at least ~20 minutes before giving up, and requeue rather than fail permanently if you hit your own ceiling. Note that distinct_profile_count and size stay null until the build finishes, so a null there is a reliable "not done yet" signal even if status momentarily reads Syncing.
4. Preview a Sample
Once completed, preview sample results before downloading the full dataset.
resp = requests.get(
f"https://api.delivr.ai/api/v1/audiences/{audience_id}/sample",
headers=HEADERS,
params={"project_id": PROJECT_ID},
)
sample = resp.json()
for row in sample["rows"][:5]:
print(f" {row.get('first_name')}")
print(f" {row.get('job_title')} at {row.get('company_name')}")
print(f" Score: {row.get('score')}, Topic: {row.get('topic_name')}")
print(f" Has business email: {row.get('has_business_email')}")
print()The sample is a capped, redacted teaser (up to 25 rows): firmographics, role, intent fields (score, topic_id, topic_name), first_name, the sha256_lc_hem match key, and has_* flags indicating which contact fields exist. Raw contact data (emails, phones, LinkedIn, personal addresses, full names) is not returned here. The full downloaded records (next step) carry the complete field set.
5. Download the Full Audience
The download endpoint prepares files and returns signed URLs. Poll until status is ready.
Watch the vocabulary. The download
statusis a separate set of values from the buildstatusin step 3. Here, onlyreadymeans the files are downloadable andfailedmeans preparation failed - every other value, includingdoneandcompleted, means still in progress. Do not reuse the build-status rule (whereCompletedis terminal) on this field, or you will try to read links before they exist.
import time
RETRYABLE_STATUS_CODES = {408, 425, 429, 500, 502, 503, 504}
class TransientDownloadError(Exception):
"""Every attempt in one burst failed for a reason worth retrying."""
def get_with_retry(url, **kwargs):
"""Back off across a short burst. A non-retryable error raises straight
through; exhausting the burst raises TransientDownloadError, which the
polling loop treats as "ask again later" rather than as a failed download.
"""
for attempt in range(5):
try:
resp = requests.get(url, timeout=(5, 30), **kwargs)
resp.raise_for_status()
return resp
except (requests.Timeout, requests.ConnectionError, requests.HTTPError) as error:
status = getattr(error.response, "status_code", None)
transient = status in RETRYABLE_STATUS_CODES or isinstance(
error, (requests.Timeout, requests.ConnectionError)
)
if not transient:
print(f"Download status request failed: {error}")
raise
if attempt == 4:
raise TransientDownloadError(
f"{attempt + 1} consecutive retryable failures: {error}"
) from error
delay = min(2**attempt, 10)
print(f"Transient request failure; retrying in {delay}s: {error}")
time.sleep(delay)
while True:
try:
resp = get_with_retry(
f"https://api.delivr.ai/api/v1/audiences/{audience_id}/download",
headers=HEADERS,
params={"project_id": PROJECT_ID},
)
except TransientDownloadError as error:
# The server being briefly unreachable says nothing about the build,
# which is still running. Only `ready` and `failed` end this loop.
print(f" Download status unavailable; still polling: {error}")
time.sleep(10)
continue
dl = resp.json()
print(f" Download status: {dl['status']}")
if dl["status"] == "ready":
break
elif dl["status"] == "failed":
print("Download failed")
break
time.sleep(3)
# Download parquet files
for partition, urls in dl["output_links"].items():
for url in urls:
if url.split("?")[0].endswith(".parquet"):
r = requests.get(url)
filename = partition.replace("/", "_") + ".parquet"
with open(filename, "wb") as f:
f.write(r.content)
print(f" Downloaded {filename} ({len(r.content)} bytes)")Download status progression: unloading -> done -> ready
Signed URLs are presigned for up to 7 days. Download promptly, and if a link ever returns an expired-signature error, call the download endpoint again to get freshly signed URLs.
Tip: Use
GET /api/v1/audiences/{id}/statusfor a lightweight file readiness check that doesn't trigger file preparation. Returnsready,unloading,no_task, orfailed. See the Intent Audiences API reference for details.
Complete Script
import time
import requests
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
ORG_ID = "your_organization_id"
PROJECT_ID = "your_project_id"
HEADERS = {
"X-Api-Key": API_KEY,
"X-Api-Secret": API_SECRET,
"Content-Type": "application/json",
}
RETRYABLE_STATUS_CODES = {408, 425, 429, 500, 502, 503, 504}
class TransientDownloadError(Exception):
"""Every attempt in one burst failed for a reason worth retrying."""
def get_with_retry(url, **kwargs):
"""Back off across a short burst. A non-retryable error raises straight
through; exhausting the burst raises TransientDownloadError, which the
polling loop treats as "ask again later" rather than as a failed download.
"""
for attempt in range(5):
try:
resp = requests.get(url, timeout=(5, 30), **kwargs)
resp.raise_for_status()
return resp
except (requests.Timeout, requests.ConnectionError, requests.HTTPError) as error:
status = getattr(error.response, "status_code", None)
transient = status in RETRYABLE_STATUS_CODES or isinstance(
error, (requests.Timeout, requests.ConnectionError)
)
if not transient:
print(f"Download status request failed: {error}")
raise
if attempt == 4:
raise TransientDownloadError(
f"{attempt + 1} consecutive retryable failures: {error}"
) from error
delay = min(2**attempt, 10)
print(f"Transient request failure; retrying in {delay}s: {error}")
time.sleep(delay)
# 1. Search for topics
resp = requests.get(
"https://api.delivr.ai/api/v1/taxonomy/topics",
headers=HEADERS,
params={"search": "cloud computing", "limit": 5, "topic_type": "B2B"},
)
topics = resp.json()["response"]["topics"]
topic_ids = [t["topic_id"] for t in topics[:2]]
print(f"Using topics: {[t['name'] for t in topics[:2]]}")
# 2. Create audience
resp = requests.post(
f"https://api.delivr.ai/api/v1/audiences?project_id={PROJECT_ID}",
headers=HEADERS,
json={
"organization_id": ORG_ID,
"project_id": PROJECT_ID,
"audience_name": f"Cloud Intent - {int(time.time())}",
"type": "intents",
"segmentation_type": "Audience",
"filter": {
"condition": "and",
"rules": [
{
"fieldName": "INTENT",
"conditionRules": {"operator": "in", "value": topic_ids},
}
],
},
},
)
audience_id = resp.json()["id"]
print(f"Created audience {audience_id}")
# 3. Poll until completed
while True:
resp = requests.get(
f"https://api.delivr.ai/api/v1/audiences/{audience_id}",
headers=HEADERS,
params={"project_id": PROJECT_ID},
)
data = resp.json()
print(f" {data['status']} (size: {data.get('size')})")
if data["status"] in ("Completed", "Validated", "Failed"):
break
time.sleep(10)
# 4. Preview
if data["status"] == "Completed":
resp = requests.get(
f"https://api.delivr.ai/api/v1/audiences/{audience_id}/sample",
headers=HEADERS,
params={"project_id": PROJECT_ID, "limit": 3},
)
for row in resp.json()["rows"][:3]:
print(f" {row.get('first_name')} {row.get('last_name')} at {row.get('company_name')}")
# 5. Download. Keep polling until the server reaches a terminal state;
# large audiences can take longer than a fixed browser timeout.
while True:
try:
resp = get_with_retry(
f"https://api.delivr.ai/api/v1/audiences/{audience_id}/download",
headers=HEADERS,
params={"project_id": PROJECT_ID},
)
except TransientDownloadError as error:
# The server being briefly unreachable says nothing about the
# build, which is still running. Only `ready` and `failed` end
# this loop.
print(f"Download status unavailable; still polling: {error}")
time.sleep(10)
continue
dl = resp.json()
if dl["status"] == "ready":
for partition, urls in dl["output_links"].items():
parquet_urls = [u for u in urls if u.split("?")[0].endswith(".parquet")]
print(f" {partition}: {len(parquet_urls)} file(s)")
break
if dl["status"] == "failed":
raise RuntimeError("Download preparation failed")
stage = dl.get("stage", "preparing")
progress = dl.get("progress")
if progress:
print(f"Download {stage}: {progress['completed']}/{progress['total']} jobs")
else:
print(f"Download {stage}: still preparing")
time.sleep(3)Notes
- Maximum 10 topics per audience.
- Preview vs. unload. The
unloadfield controls what a build writes.previewcomputes size and stats and a small capped sample;unloadwrites the full downloadable dataset. If you intend to download the whole audience, create it with"unload": "unload". Either way the build still reachesCompleted, so checkunloadmode (or just drive the download endpoint in step 5, which prepares the full files on demand) rather than assumingCompletedmeans a full dataset is already on disk. - The
scorefilter is optional. If omitted, all intent strengths (high, medium, low) are included. segmentation_typemust be one of:"Audience","Persona","Account".- Response shape differs by resource. Taxonomy endpoints wrap their payload in a
responseenvelope (resp.json()["response"]["topics"]), while audience endpoints return the object at the top level (resp.json()["id"],resp.json()["status"]) - there is noresponse.audiencewrapper. Read audience fields directly off the body. - The Taxonomy API uses
api.delivr.ai. The Audiences API usesapi.delivr.ai. - Download files are in Parquet format. See Reading Parquet Files for how to convert them to CSV or open them in Excel.
Next Steps
- Understanding Segmentation Types: When to use Persona vs Account vs Audience
- Building Audience Filters: Complete field and operator reference
- Reading Parquet Files: Convert downloaded files to CSV or open in Excel
- Intent Audiences API: Full API reference (filters, operators, listing, schema)
- Taxonomy API: Browse the full topic catalog (19,500+ topics)
- On-Domain Events API: Query pixel visitor events instead of intent audiences
Updated 1 day ago
