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",
"topics": [
{
"topic_id": "4eyes_103456",
"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. That is a normal outcome, never an error.scoreis the intent tier (highormedium);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.as_ofis the single calendar day the intent dataset covers. See below.- You can send
linkedin_urlinstead ofemail. A LinkedIn URL identifies the person rather than one address, so it resolves across every address we hold for them and can match where a single email would not.
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, and
matched: falseis the correct answer for that day. - Only
highandmediumtiers are published. Low-intent activity is dropped upstream, so an emptytopicsarray means no strong signal on that day, not no activity at all. - Each person carries at most 50 topics on a given day. For someone researching heavily, the response is the strongest 50, 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. Unmatched items simply come back matched: false.
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. 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
| 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 |
429 | Over 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/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. - 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 3 days ago
