Skip to content
Pathbound DOCS

Batch & Aggregate

When you need to look up many contacts at once, or roll events up by some dimension, paginating the list endpoints is wasteful. Use the batch and aggregate endpoints instead — both perform the work server-side and return a single response.

POST /v1/batch/details

Required scope: contacts:read (works for contacts and companies); entity_type: "deals" also needs deals:read.

Fetch up to 25 contacts, companies or deals by ID in one request. Reading a record activates its customer for the billing cycle, as the single-record endpoints do; a deal activates the company (or, failing that, the contact) it belongs to. Pass dry_run: true to learn how many new activations a read would cost without reading or metering anything.

FieldTypeRequiredDescription
entity_typestringyescontacts, companies or deals.
idsstring[]yesArray of 1–25 IDs.
dry_runbooleannoReturn { dry_run, match_count, estimated_activations } instead of the records. Never meters.
Terminal window
curl -X POST https://api.pathbound.ai/v1/batch/details \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"entity_type": "contacts",
"ids": ["665f1c2ab3d4e5f601234567", "665f1c2ab3d4e5f601234568", "665f1c2ab3d4e5f601234569"]
}'
{
"status": "success",
"contacts": [ /* up to 25 contact objects */ ]
}

IDs that don’t exist (or belong to a different tenant) are silently dropped from the response.


POST /v1/batch/activity

Required scope: contacts:read.

Fetch recent events for up to 25 contacts in one request — without one busy contact starving the others. Each contact gets up to events_limit events independently.

FieldTypeRequiredDescription
contact_idsstring[]yesArray of 1–25 contact IDs.
events_limitnumbernoMax events per contact (1–20, default 5).
date_afterstringnoISO 8601 lower bound on timestamp.
date_beforestringnoISO 8601 upper bound on timestamp.
{
"status": "success",
"contact_count": 3,
"activity": {
"665f1c2ab3d4e5f601234567": {
"events": [
{ "id": "665f1c2ab3d4e5f6012345e1", "event": "page_view", "url": "https://example.com/pricing", "timestamp": "...", "title": "Pricing" }
],
"total_events": 18
},
"665f1c2ab3d4e5f601234568": { "events": [], "total_events": 0 },
"665f1c2ab3d4e5f601234569": { "events": [ /* … */ ], "total_events": 4 }
}
}

Empty arrays are returned for contacts with no events in the window.


POST /v1/meetings/query

Required scope: contacts:read. A company filter or a companies segment also needs companies:read.

Read many tl;dv recordings in one request, across customers, with each meeting’s AI notes cut down to the sections you ask for. Calendly bookings aren’t included; list those per customer with Get Contact Conversations.

Filters combine with AND. With no company, contact, or segment filter, every customer call in the workspace matches. Calls with only your own team on them are left out, and your team never appears in a meeting’s contact_ids. Your team means workspace members’ addresses, their company domains, your verified domains, and the internal domains and email patterns in your workspace settings.

FieldTypeDefaultDescription
company_idsstring[]—Only meetings attended by someone at these companies (1–25).
contact_idsstring[]—Only meetings these contacts attended (1–25).
segmentstring—Saved segment id or handle. A companies segment matches anyone at its companies.
meeting_idsstring[]—Only these meetings (1–25).
date_afterstring—ISO 8601. Only meetings on or after this date.
date_beforestring—ISO 8601. Only meetings on or before this date.
has_notesboolean—true for meetings with AI notes, false for meetings without.
has_transcriptboolean—true for meetings with a transcript, false for meetings without.
sectionsstring[]—Note sections to return (1–10). See Sections. Omit to get section_catalog instead of notes.
sort_dirstringdescBy meeting date, asc or desc.
offsetnumber0Meetings to skip. Pass the previous page’s next_offset.
limitnumber25Most meetings per page (1–50).
max_charsnumber9000Page size in characters (2,000–50,000).
count_onlybooleanfalseReturn only { "count": n }.
dry_runbooleanfalseReturn match_count and estimated_activations for the page without reading it.

tl;dv writes notes from your workspace’s notes template: markdown headings such as 4) Commercials, and field labels such as Top pains (ranked): inside them. Many meetings carry a topic list with bullets instead. sections accepts:

  • A heading or field name. Matching ignores case, numbering, and punctuation, and a name can be part of a longer one: pains finds Top pains (ranked). A heading returns everything under it. On a meeting that only has topics, names match topic titles.
  • topics returns the topic list with its bullets.
  • all returns the whole notes.

A requested name that a meeting doesn’t have is listed in that meeting’s missing_sections. Without sections, the response carries section_catalog: the section names on the page and how many meetings have each, most common first.

Pages are cut by size, so a page can hold fewer than limit meetings. Pass each response’s next_offset back as offset until next_offset is null. A meeting that doesn’t fit a page on its own comes back alone, with its longest sections cut and marked "truncated": true. Read it again with meeting_ids and fewer sections, or with a larger max_chars.

{
"status": "success",
"meetings": [
{
"_id": "6650f1c2e4b0a1b2c3d4e5f6",
"name": "Acme discovery",
"happened_at": "2026-09-22T15:00:00.000Z",
"duration_seconds": 2280,
"organizer": { "email": "[email protected]", "name": "Dana Lee" },
"invitees": [{ "email": "[email protected]", "name": "Sam Ortiz" }],
"contact_ids": ["665f1c2ab3d4e5f601234567"],
"has_transcript": true,
"has_notes": true,
"notes_format": "markdown",
"sections": [
{ "name": "Top pains (ranked)", "text": "1. Dispatch is manual\n2. No route history" }
],
"missing_sections": ["Decision process"]
}
],
"offset": 0,
"returned": 1,
"next_offset": 1
}

contact_ids are the customers on the call, within your filter. Pass one with the meeting _id to Get Meeting Transcript. notes_format is markdown, topics, or none.

Each customer on a returned meeting counts as a profile activation for the billing cycle, once per company, as with the other endpoints that return customer records. Your team never counts. count_only and dry_run never activate anything.


POST /v1/aggregate

Required scope: depends on entity — contacts:read, companies:read, deals:read, or events:read.

Group-by aggregation across contacts, companies, deals, or events, with optional filters and metrics. Aggregations never activate a profile. This is the engine behind dashboard charts and aggregate_data in MCP.

FieldTypeRequiredDescription
entitystringyescontacts, companies, deals, or events.
group_bystringyesField to group on (e.g. lifecycle_stage, industry, country, stage, event_type, day).
filtersobjectnoEntity-specific filters applied before grouping (see below).
field_filtersarraynoTyped filters on canonical or materialized source fields: [{ "field", "op", "value" }], max 10.
segmentstringnoScope to a saved segment (id or handle). Contacts and companies only.
metricstringnocount (default), avg, sum, min, max.
metric_fieldstringnoRequired for non-count metrics — the field to aggregate.
limitnumbernoMax buckets returned (1–50, default 25).
  • Contacts: lifecycle_stage, lead_status, industry, country, state, city, company, company_id, job_title, email_verification_state, tag, segment_id, source, or a materialized source field written as source.field (e.g. apollo.industry).
  • Companies: industry, country, state, size_bucket, revenue_bucket, tag, segment_id, source, or a materialized source field.
  • Deals: stage, status, currency, company_id, contact_id, close_month, amount_bucket, tag.
  • Events: event_type, domain, url, url_path, title, text, element_type, element_id, class, contact_id, visitor_id, session_id, company_id, day, week, month, device_type, browser, os, platform, referring_domain, is_bot.

An unknown group_by (or another invalid combination, such as segment on deals) is a 400 with code invalid_aggregate and the reason in the message.

size_bucket and revenue_bucket are derived buckets — 1-10 / 11-50 / 51-200 / etc. for numberofemployees, and <$1M / $1M-$10M / etc. for annualrevenue. day, week, and month are derived from each event’s timestamp. class groups on the full CSS class string, which is noisy (hashed tokens) — for targeting a specific element prefer the class_contains filter with a stable token.

Text buckets are lowercased (united states). Deal stage buckets merge case variants but show a stored spelling (Closed Won). An (unknown) bucket means the field is empty; to select those records use field_filters with { "field": "country", "op": "exists", "value": false }.

  • Contacts: company (substring), company_id, industry, lifecycle_stage, lead_status, city, state, country, job_title (substring), created_after, created_before, is_identified, email_verification_state, email_verified_after, email_verified_before, search, has_linkedin, tags_any, tags_all, data_sources (comma string or array), segments_any (ids or handles, static or dynamic), exclude_internal, contact_ids (capped at 100).
  • Companies: name (substring), domain (substring), industry, country, state, min_employees, max_employees, search, tags_any, tags_all, data_sources, segments_any, exclude_internal.
  • Deals: name (substring), status, stage, currency, company_id, contact_id, min_amount, max_amount, close_date_after, close_date_before, created_after, created_before, tags_any, tags_all.
  • Events: event_type, event_types (array or comma-separated string), domain, url, url_path, url_contains, text, text_contains, title, title_contains, element_type, element_id, class_contains, contact_id, contact_ids, visitor_id, session_id, referring_domain, device_type, browser, os, platform (web, ios or android; anything else is a 400), is_bot, identified_only, date_after, date_before.

Labels such as country, state, city, industry, lifecycle_stage and deal stage match the whole value, case-insensitively: "united states" (as a bucket shows it) finds United States, and "US" does not find Australia. The same holds for the list endpoints’ filters. Filter keys that aren’t listed here are not applied; the response names them in ignored_filters.

Example — events per day for the last 30 days

Section titled “Example — events per day for the last 30 days”
Terminal window
curl -X POST https://api.pathbound.ai/v1/aggregate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"entity": "events",
"group_by": "day",
"filters": {
"event_type": "page_view",
"date_after": "2026-03-30T00:00:00Z"
},
"metric": "count",
"limit": 30
}'
{
"status": "success",
"entity": "events",
"group_by": "day",
"total_matching": 11900,
"distinct_groups": 30,
"buckets": [
{ "day": "2026-04-28", "count": 511 },
{ "day": "2026-04-29", "count": 432 }
]
}

Each bucket is keyed by the group_by field, with a count (and, for a non-count metric, a <metric>_<metric_field> value). Contacts, companies and deals return total_records (everything in the workspace) and total_matching (records matching your filters); events return total_matching. With group_by: "source", buckets overlap (a record can carry several sources), so they sum to more than total_matching.

Event aggregations additionally return distinct_groups — the total number of unique group_by values, not capped by limit. This answers “how many distinct X” in the same call: group_by=visitor_id with filters: { "event_type": "button_click", "text": "Go!" } returns total_matching (how many Go! clicks) alongside distinct_groups (how many distinct visitors clicked it at least once). Use group_by=contact_id to count distinct identified contacts instead of anonymous visitors.