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')} {row.get('last_name')}")
    print(f"    {row.get('job_title')} at {row.get('company_name')}")
    print(f"    Score: {row.get('score')}, Topic: {row.get('topic_name')}")
    print()

Each row contains ~111 fields including contact info, company data, demographics, and intent fields (score, topic_id, topic_name, category, subcategory).

5. Download the Full Audience

The download endpoint prepares files and returns signed URLs. Poll until status is ready.

Watch the vocabulary. The download status is a separate set of values from the build status in step 3. Here, only ready means the files are downloadable and failed means preparation failed - every other value, including done and completed, means still in progress. Do not reuse the build-status rule (where Completed is terminal) on this field, or you will try to read links before they exist.

import time

while True:
    resp = requests.get(
        f"https://api.delivr.ai/api/v1/audiences/{audience_id}/download",
        headers=HEADERS,
        params={"project_id": PROJECT_ID},
    )
    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}/status for a lightweight file readiness check that doesn't trigger file preparation. Returns ready, unloading, no_task, or failed. 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",
}

# 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
    for _ in range(60):
        resp = requests.get(
            f"https://api.delivr.ai/api/v1/audiences/{audience_id}/download",
            headers=HEADERS,
            params={"project_id": PROJECT_ID},
        )
        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
        time.sleep(3)

Notes

  • Maximum 10 topics per audience.
  • Preview vs. unload. The unload field controls what a build writes. preview computes size and stats and a small capped sample; unload writes the full downloadable dataset. If you intend to download the whole audience, create it with "unload": "unload". Either way the build still reaches Completed, so check unload mode (or just drive the download endpoint in step 5, which prepares the full files on demand) rather than assuming Completed means a full dataset is already on disk.
  • The score filter is optional. If omitted, all intent strengths (high, medium, low) are included.
  • segmentation_type must be one of: "Audience", "Persona", "Account".
  • Response shape differs by resource. Taxonomy endpoints wrap their payload in a response envelope (resp.json()["response"]["topics"]), while audience endpoints return the object at the top level (resp.json()["id"], resp.json()["status"]) - there is no response.audience wrapper. Read audience fields directly off the body.
  • The Taxonomy API uses api.delivr.ai. The Audiences API uses api.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


Did this page help you?