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 lookup 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: ...
New keys default to Test. A test key returns sample data, and its dry runs return sample counts. Every response carries
X-Delivr-Mode: test. For real data and real billable counts, create a Live key. A dry run on a live key bills nothing and needs no payment method.
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_118056",
"topic_name": "CRM Software",
"score": "high",
"perc_score": 87
}
]
},
"as_of": "2026-06-19"
}How to read it:
idis your own opaque row identifier, echoed back. Use it to join results to your source data.matched: falsewith an emptytopicsarray means the person is not in the current intent data. An unmatched person is a normal outcome, never an error.matched_onnames the identifier that was resolved (person_id,hem,emailorlinkedin_url).person_idis 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 orhemalongside it. Do not confuseperson_idwith theprofile_pid_allcolumn in audience exports. It is returned whenever the identifier resolved to a person. Send it back asperson_idon 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,hemorlinkedin_urlresolves to the person who holds it.topicscovers every address we hold for them. An address with no intent of its own still matches when another of the person's addresses has intent. resolved_across_addresses: truemarks ahemoremailmatch 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 forlinkedin_urlorperson_idmatches.- An opted-out identifier never matches.
scoreis the intent tier (high,mediumorlow).perc_scoreis 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 strongperc_scorecan sit next to amediumtier for people who research thousands of topics. See Intent Audience Fields.as_ofis the single calendar day the intent dataset covers. See below.- You can send
linkedin_urlinstead ofemail. A LinkedIn URL identifies the person, who is matched across every address we hold for them, the same as an email or hem. - A
hemoremailthat returns noperson_idmatches on its own intent. For such an address,resolve_person: truealso matches the addresses linked to the one you sent. Without it, only the address you sent is matched.
What as_of means
as_of meansas_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. In that case,
matched: falseis 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, use an Intent Audience. It reads its configured range (a relative intent_window_days, or a fixed intent_start_date-intent_end_date window). Depending on row_grain, it keeps either each person's single strongest topic across the range 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?.
Merged and unusable topic IDs
If you send topic_ids, you can include a topic ID that was merged into another topic. The replacement topic answers, and the row keeps the ID you sent in topic_id and names the answering topic in resolved_topic_id. If the API cannot use a topic ID, the request still returns 200 and the response adds a top-level warnings array with one entry for each ID. Branch on code, which is unknown_topic_id, numeric_topic_id or retired_topic_id. The array is omitted when every ID is usable.
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. Send it with a live key, because a test key returns a sample count:
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 one record for each item that returns at least one topic, counted after topic_ids and min_score. A lookup that does not match, or that matches but returns no topics, costs nothing. Because any address a person holds resolves them, more lookups can match than when each address was read on its own. 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}`);
}Can I get intent for a visitor the pixel resolved?
Yes, in two calls. Neither the identity lookup (GET /api/v1/lookup) nor the pixel event rows carry intent topics or scores. Intent is served only by the /api/v1/intent/* endpoints, so the pixel gives you the person and the intent lookup gives you what they research.
- Read the visitor's
hem(oremail, orlinkedin_url) from the resolved event, as described in Identify Site Visitors. - Send it to
POST /api/v1/intent/lookup, or up to 10,000 at once to/api/v1/intent/batch:
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": "event-8911fff2", "hem": "YOUR_64_CHARACTER_SHA256_HEM"}'A hem must be the SHA-256 digest in 64 hex characters. A hash in any other algorithm, such as MD5, is not used for the match. Send the email or linkedin_url instead if that is what you hold. The response is the one described above: topics with score and perc_score, plus as_of.
You are billed one record only when the response returns at least one topic. A visitor with no intent, or with none of the topics you asked for, costs nothing. To see the cost for a set of visitors first, send "dry_run": true.
To get intent for everyone who visited rather than one visitor at a time, build a website visitor audience. Then build an Intent Audience whose filter combines your INTENT rules with a WEBSITE_VISITORS rule naming that visitor audience. A visitor audience on its own carries no intent topics.
Errors you might see
| Status | Meaning | Fix |
|---|---|---|
400 | Malformed request (missing email/linkedin_url, batch over 10,000 items) | Check the request body against the examples above |
401 | Missing or invalid key/secret pair | Re-copy both halves from the dashboard. See Authentication |
402 with error payment_method_required | Your organization has no card or bank account on file, so paid requests are refused (previews and dry_run requests still work) | An organization owner or admin adds one in the dashboard under Billing, then retry. Other 402 codes are listed in Rate Limits and Metering |
429 with error platform_cap_exceeded | Over a daily or monthly usage limit | Do 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 429 | Over your request rate limit (default 10 requests/sec per organization) | Wait for Retry-After, then 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/companyscores 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/peopleand/api/v1/intent/companies. Free capped previews exist at/previewvariants so you can see real records before importing. The topic quickstart walks through it. - Which API should I use? if your use case is not a per-row lookup.
- Full API reference for every field on every endpoint.
Updated about 24 hours ago
