Quickstart: Enrich Emails with Intent

What this does

Send an email address (or a LinkedIn URL), get back the intent topics that person is actively researching, with scores. This is the fastest path to a working Delivr integration: one endpoint, one POST, useful data in the response.

email in  ->  POST /api/v1/intent/lookup  ->  topics + scores out

Typical uses: scoring leads in a CRM, an enrichment column in Clay or a spreadsheet workflow, prioritizing outreach by what a prospect is researching.

This endpoint answers from a single day of intent data, the day named by as_of in the response. It is not a rolling week. See What as_of means below before comparing a lookup against anything built over a longer window, such as an Intent Audience.

Before you start

You need an organization API key + secret pair. Create one in the dashboard under Settings > API keys; the Authentication guide walks through it. Every request below sends both headers:

X-Api-Key: dlvr_...
X-Api-Secret: ...

1. Look up one person

curl -X POST "https://api.delivr.ai/api/v1/intent/lookup" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"id": "row-42", "email": "[email protected]"}'

Response (200 OK)

{
  "response": {
    "id": "row-42",
    "matched": true,
    "matched_on": "email",
    "person_id": "c41f6a0d92b7e5381a4d7f0e63b9a2c58d1e9f47b30a6c25e8d7149fa3b06e21",
    "topics": [
      {
        "topic_id": "4eyes_103456",
        "topic_name": "CRM Software",
        "score": "high",
        "perc_score": 87
      }
    ]
  },
  "as_of": "2026-06-19"
}

How to read it:

  • id is your own opaque row identifier, echoed back. Use it to join results to your source data.
  • matched: false with an empty topics array means the person is not in the current intent data. That is a normal outcome, never an error.
  • matched_on names the identifier that was resolved (person_id, hem, email or linkedin_url).
  • person_id is an id for the person (64 lowercase hex characters, opaque to you). A small share of ids change when our identity data is rebuilt, so keep the email or hem alongside it, and do not confuse it with the profile_pid_all column in audience exports. It is returned whenever the identifier resolved to a person. Send it back as person_id on a later lookup, or on a batch item, to read the same person directly. It takes precedence over any other identifier on the same item.
  • Intent is matched to the person, not to one address. An email, hem or linkedin_url resolves to the person who holds it, and topics covers every address we hold for them, so an address with no intent of its own still matches when another of the person's addresses has intent. resolved_across_addresses: true marks a hem or email match where the person holds at least one address other than the one you sent; the other address does not need intent of its own. It is not set for linkedin_url or person_id matches.
  • An opted-out identifier never matches.
  • score is the intent tier (high, medium or low); perc_score is a 1-99 strength rating for that person on that topic, derived from their own recent activity on it. It is not a percentile and not a rank against other people, so repeated values are expected. A strong perc_score can sit next to a medium tier for people who research thousands of topics; see Intent Audience Fields.
  • as_of is the single calendar day the intent dataset covers. See below.
  • You can send linkedin_url instead of email. A LinkedIn URL identifies the person, who is matched across every address we hold for them, the same as an email or hem.
  • An address we cannot resolve to a person matches on its own intent and returns no person_id. For those addresses, resolve_person: true still widens the match to the person's other addresses.

What as_of means

as_of names one calendar day, not a cut-off with history behind it. The intent dataset is rebuilt from that day's activity and replaced whole, every day. Three consequences worth knowing before you compare results:

  • A person can match one day and not the next, without anything having changed about them or about your request. If someone researched a topic on the 26th, a lookup served from the 1st will not show it, and matched: false is the correct answer for that day.
  • All three tiers are returned by default. Send min_score: "medium" or "high" to drop the weaker tiers.
  • Each person carries at most 1,000 topics on a given day. For the small share of people researching more than that, the response is the strongest 1,000, not an exhaustive list.

If you need activity across a range of days rather than one, that is what an Intent Audience builds: it reads its configured range (a relative intent_window_days, or a fixed intent_start_date-intent_end_date window) and, depending on row_grain, keeps either each person's single strongest topic across it or every topic they matched. The two can legitimately disagree for the same person on the same day, because they measure different spans. See Which API should I use?.

2. Look up a list (up to 10,000 per call)

POST /api/v1/intent/batch takes up to 10,000 items per request, mixing email and linkedin_url freely. Results come back in input order, one per item.

curl -X POST "https://api.delivr.ai/api/v1/intent/batch" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {"id": "r1", "email": "[email protected]"},
      {"id": "r2", "linkedin_url": "https://www.linkedin.com/in/jane-doe-87745243"},
      {"id": "r3", "email": "[email protected]"}
    ]
  }'

The response carries matched_count plus a results array shaped like the single lookup, including person_id on each item that resolved to a person. Unmatched items simply come back matched: false. A batch item can also carry a person_id instead of an email or LinkedIn URL.

3. Preview cost before you run (dry_run)

Every intent endpoint accepts "dry_run": true. The API resolves your request, tells you exactly how many records a live run would bill, and charges nothing:

curl -X POST "https://api.delivr.ai/api/v1/intent/batch" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"items": [...], "dry_run": true}'
{
  "dry_run": true,
  "meter": "intent_signal_scored",
  "records_to_charge": 2
}

You are billed per matched record only; unmatched lookups cost nothing. Because any address a person holds resolves them, more lookups can match than when each address was read on its own, so a dry_run count can be higher than before. The people-by-topic endpoints bill one row per person. Details in Rate Limits and Metering.

Complete example: enrich a CSV (Python)

import csv
import requests

API = "https://api.delivr.ai/api/v1/intent/batch"
HEADERS = {
    "X-Api-Key": "YOUR_API_KEY",
    "X-Api-Secret": "YOUR_API_SECRET",
}
BATCH_SIZE = 10_000  # server-side maximum per call

def batches(rows, size):
    for i in range(0, len(rows), size):
        yield rows[i : i + size]

with open("leads.csv") as f:
    rows = [{"id": str(i), "email": r["email"]} for i, r in enumerate(csv.DictReader(f))]

enriched = {}
for chunk in batches(rows, BATCH_SIZE):
    resp = requests.post(API, headers=HEADERS, json={"items": chunk}, timeout=60)
    resp.raise_for_status()
    for result in resp.json()["response"]["results"]:
        if result["matched"]:
            top = result["topics"][0] if result["topics"] else None
            enriched[result["id"]] = {
                "top_topic": top["topic_name"] if top else "",
                "score": top["score"] if top else "",
                "topic_count": len(result["topics"]),
            }

print(f"matched {len(enriched)} of {len(rows)}")

Complete example: enrich one row (JavaScript)

const resp = await fetch('https://api.delivr.ai/api/v1/intent/lookup', {
  method: 'POST',
  headers: {
    'X-Api-Key': process.env.DELIVR_API_KEY,
    'X-Api-Secret': process.env.DELIVR_API_SECRET,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ id: 'lead-1', email: '[email protected]' }),
});
if (!resp.ok) throw new Error(`Delivr API ${resp.status}`);

const { response } = await resp.json();
if (response.matched) {
  const researching = response.topics.map((t) => t.topic_name).join(', ');
  console.log(`Researching: ${researching}`);
}

Errors you might see

StatusMeaningFix
400Malformed request (missing email/linkedin_url, batch over 10,000 items)Check the request body against the examples above
401Missing or invalid key/secret pairRe-copy both halves from the dashboard; see Authentication
429 with error platform_cap_exceededOver a daily or monthly usage limitDo not retry straight away: kind says whether to wait for resets_at, send a smaller request, or contact support; see Rate Limits and Metering
Any other 429Over your request rate limit (default 10 requests/sec per organization)Back off and retry; see Rate Limits and Metering

An unmatched identifier is not an error: it returns 200 with matched: false.

Where to go next

  • Company-level intent: POST /api/v1/intent/company scores a company domain the same way. Same auth, same shape.
  • Discovery (topic to people/companies): given a topic, list the people or companies showing intent with POST /api/v1/intent/people and /api/v1/intent/companies. Free capped previews exist at /preview variants so you can see real records before importing.
  • Which API should I use? if your use case is not a per-row lookup.
  • Full API reference for every field on every endpoint.

Did this page help you?