Events API v3

The Events API v3 splits the events query into four route-fixed tiers. The URL you call fixes both the billing tier and the identity fields you can get back. The price of a call is therefore legible from the route. Pick the tier that matches the enrichment you need.

Pick the route, and you pick the price

RouteReturns
GET /api/v3/events/web_eventBase event columns, no identity resolution (lowest cost)
GET /api/v3/events/resolution_hemAdds the hashed email (HEM)
GET /api/v3/events/resolution_hem_pteAdds the HEM plus the plaintext email (email) and UUID/PID stitching identifiers (PTE)
GET /api/v3/events/resolution_full_profileAdds full contact, company, and demographic fields (highest cost)

There is no bare /api/v3/events query route: you always call a tier. Start with web_event to confirm data is flowing, then move up only to the tier whose identity or profile fields you actually need.

Rate limit and billing

  • The Events API allows 720 requests per minute per organization, with a burst of 25. Over the limit returns 429 with a Retry-After header. Higher limits are available per contract.
  • You are billed per record returned on a 2xx response, on the route's meter. Schema routes, the counts route, empty results, and non-2xx responses bill nothing.

See Rate Limits and Metering for the full rules.

Discover the fields before you query

  • GET /api/v3/events_schema returns the full field metadata (non-billable).
  • GET /api/v3/events/{tier}/schema returns only the fields allowed at that tier, for example GET /api/v3/events/web_event/schema.

Selecting a field above your route's tier is rejected, so the schema endpoint tells you exactly what each tier can return.

Authenticate and scope

Send your API key on every request, and scope the call to a project:

X-Api-Key: dlvr_xxxxxxxx
X-Api-Secret: <your secret>

Pass the project as ?project_id=<project-uuid> (or the X-Project-Id header). Create your key in the organization's Settings > Members & access > API keys. All calls are served from https://api.delivr.ai. The apiv2.delivr.ai and apiv3.delivr.ai hostnames are legacy aliases of the same backend, so the path selects the version, not the host.

A project-scoped key reads only its own project. A request scoped to the organization returns that project's events. Another project's project_id, pixel or campaign returns 404.

Query

curl -s "https://api.delivr.ai/api/v3/events/web_event?project_id=<project-uuid>&start_ms=1792022400000&end_ms=1792108800000&limit=100" \
  -H "X-Api-Key: dlvr_xxxxxxxx" \
  -H "X-Api-Secret: <your secret>"

The response is { "rows": [ ... ], "meta": { ... } }. The columns present depend on the tier route and the fields you select.

start_ms and end_ms are required on every tier route. A request without them returns 400. The window is at most 25 hours, or 92 days with include_cold=true. limit defaults to 300 and is capped at 2000: a larger value returns 400. distinct takes a single field, not a comma list.

To see what a query would bill before you run it, add dry_run=true. The response is { "dry_run": true, "meter": "...", "records_to_charge": N }, and nothing is billed. records_to_charge is an upper bound, counted before filter and before email opens are left out.

Move up a tier to get resolved identity:

curl -s "https://api.delivr.ai/api/v3/events/resolution_hem?project_id=<project-uuid>&start_ms=1792022400000&end_ms=1792108800000&limit=100" \
  -H "X-Api-Key: dlvr_xxxxxxxx" \
  -H "X-Api-Secret: <your secret>"

List the visitors behind the events

The tier routes return one row per event. When you want who visited rather than what they did, call a tier's /visitors route. It returns one row per person, or per company, over a window, deduplicated across the project's pixels.

curl -s "https://api.delivr.ai/api/v3/events/resolution_full_profile/visitors?project_id=<project-uuid>&kind=buyers&start_ms=1790121600000&end_ms=1790726400000&limit=500" \
  -H "X-Api-Key: dlvr_xxxxxxxx" \
  -H "X-Api-Secret: <your secret>"

The response is { "visitors": [ ... ], "count": 500, "has_partial_data": false }.

  • Buyers or accounts. kind=buyers returns one row per person: rows that share a profile id or the same hem are merged. Each hashed email in hems is a separate candidate person, so rows that share only a hems entry stay separate. kind=accounts returns one row per company, deduplicated on company_domain. Only resolution_full_profile carries the company domain, so accounts are available on that tier alone.
  • The tier is the price. Visitors are available on resolution_hem, resolution_hem_pte and resolution_full_profile. Each visitor returned counts once at that tier's price, whatever columns you select. Selecting fewer columns makes the answer smaller, not cheaper. web_event has no visitors route, because it carries no identity to deduplicate on.
  • Scope. Name project_id to read every pixel in the project, or pixel_id for one. Add include_dsp=true to include visitors from the project's DelivrDSP campaigns as well. Campaigns are read only for a project_id: with pixel_id alone, include_dsp adds nothing, so name the project too.
  • Window and limit. start_ms, end_ms and limit are required, and limit goes up to 50,000. Visitors come most recent first, and limit keeps the most recent. There is no paging: when more visitors match than you asked for, narrow the window to reach earlier ones.
  • Partial answers. has_partial_data: true means part of the window could not be read, so visitors may be missing. Only the visitors returned count. Ask again, or narrow the window.

List only the people you have not received this month

GET /api/v3/events/{tier}/visitors/new is the visitors route for people who are new to you. It returns the same rows as /visitors, minus anyone already delivered to this project, at the same tier, this UTC calendar month. Use it to pull a list on a schedule without paying for the same person twice. It is available on resolution_hem, resolution_hem_pte and resolution_full_profile.

curl -s "https://api.delivr.ai/api/v3/events/resolution_full_profile/visitors/new?project_id=<project-uuid>&kind=buyers&start_ms=1790121600000&end_ms=1790726400000&limit=500" \
  -H "X-Api-Key: dlvr_xxxxxxxx" \
  -H "X-Api-Secret: <your secret>" \
  -H "Idempotency-Key: 2026-10-weekly-pull-1"

The response is the /visitors response plus these fields:

{
  "visitors": [ ... ],
  "count": 500,
  "has_partial_data": false,
  "month": "202610",
  "already_delivered_this_month": 1240,
  "replayed": 0,
  "busy_elsewhere": 0
}
  • Request rules. project_id is required (or a key bound to one project), and kind must be buyers. pixel_id and dry_run return 400, because who is new is decided when the rows are delivered. The other /visitors parameters, start_ms, end_ms, limit, select, filter and include_dsp, work as they do there.
  • month. The UTC calendar month the answer was counted in, as YYYYMM. It resets at 00:00 UTC on the 1st. See what "month" means.
  • already_delivered_this_month. People in the window you were already sent this month. They are left out and not billed.
  • replayed. People this same request already delivered, returned again on a retry. See Idempotency-Key below.
  • busy_elsewhere. People left out because another request is delivering them right now. Ask again later to receive them if that request does not complete.
  • Idempotency-Key. Optional header, one value of 1 to 255 printable ASCII characters. A retry with the same key, project and tier returns the people the first attempt delivered again, counted in replayed, and bills nothing more for them. Use a new key for a new pull. A second request with a key whose first request is still running gets 409. Retry once it completes. Without a key, every request is its own pull.
  • Continuing a long window. A request that scans a very large window stops early. When has_partial_data is true and next_end_ms is present, everything from next_end_ms up to your end_ms was scanned. Repeat the call with end_ms set to next_end_ms and the same start_ms to carry on. has_partial_data: true without next_end_ms means part of the window could not be read. Ask again or narrow the window.
  • Billing. A person is billed once per project, per tier, per UTC calendar month, the first time they are delivered by a new-only surface. The /visitors routes still bill every row they return. People are billed after the response is delivered. A response that fails to send bills nothing and leaves those people new.
  • Temporary errors. A 503 means nothing was delivered or billed. Retry.

Count and break down one day of events

When you want to know how much traffic a day carried and where it came from, call GET /api/v3/events/web_event/counts. This route gives you counts, rather than the events themselves. It counts a project's or one pixel's events over a window of up to 24 hours. It breaks the counts down by up to two fields. It returns counts only, never identifiers, and bills nothing.

curl -s "https://api.delivr.ai/api/v3/events/web_event/counts?project_id=<project-uuid>&start_ms=1792022400000&end_ms=1792108800000&group_by=ip_country&limit=3" \
  -H "X-Api-Key: dlvr_xxxxxxxx" \
  -H "X-Api-Secret: <your secret>"

The response carries the window and scope you asked for, plus { "totals": { "events": 2438, "visitors": 120, "resolved": 30 }, "groups": [ { "key": { "ip_country": "US" }, "events": 1290, "visitors": 81, "resolved": 27 }, ... ] }. Without group_by, groups is empty.

  • Measures. events counts events, visitors counts distinct browsers, and resolved counts events resolved to a person. totals covers every matching event. groups holds the top limit groups (10 by default, up to 50), largest first by order_by.
  • Breakdowns. group_by takes up to two of pixel_id, event_type, ip_country, is_bot, bot_type, resolved, gpc and browser. filter takes the usual grammar, limited to fields the web_event tier carries. Email opens are left out of the counts unless the filter names event_type email_open.
  • Browser ranking. group_by=browser ranks individual browsers by activity. Each comes back as a rank, such as 1 or 2, never as a cookie ID. Events without a browser cookie form one unranked group, with browser set to null.
  • Window. At most 24 hours, starting no more than 30 days ago. A window with more stored events than one request can read returns 422 with code set to scan_too_large, and a message giving both numbers. Narrow to one pixel, or shorten the window.
  • Freshness. Counts cover events the event store has already written, which it does within an hour of their arrival. A window that includes the most recent hour can come back short for that hour, so ask again later for a complete count.
  • Errors. Every error carries a message in error and a reason in code. Branch on code:
codeStatusMeaning
invalid_request400A parameter is missing or invalid. The message names it.
upstream_rejectedthe event store's 4xx, usually 400The event store refused the query, for example a filter with an unsupported operator. With 408, the query ran too long: narrow the window, the pixel or the filter.
not_found404The project or pixel was not found in your organization, or the pixel is not in the named project.
method_not_allowed405The route takes GET only.
pixel_inactive422The pixel is inactive.
scan_too_large422The window holds more stored events than one request can read.
internal_error500Access could not be checked. Retry.
upstream_error502, or the event store's 5xxThe event store failed or is overloaded. Retry.
unavailable503Counts are temporarily unavailable. Retry shortly.
upstream_timeout504The event store did not answer in time. Retry, or narrow the window.

A missing or invalid API key (401) and rate limiting (429) are answered before the route runs, so those carry error only.

Filters, field selection, and time windows

The tier routes share the same query parameters as the rest of the events API: field selection, filters, ordering, paging, and the time window. See Pixel and DSP Event Fields for the full field catalog and query-parameter reference.

Which endpoint should I use?

The per-tier routes make cost predictable and bound the returned fields to the tier you called. Start at web_event to confirm data is flowing, then move up only to the tier whose identity or profile fields you actually need.

To have resolved visitors pushed to your own endpoint every 5 minutes instead of polling these routes, see Receive Website Visitors by Webhook. Its new-only mode shares the once-per-month ledger with /visitors/new.


Did this page help you?