Contacts REST API
Contacts represent people in your Pathbound workspace. Each contact has a unified profile, an event timeline, conversation history, and a list of notes. Contacts are associated with a company and have a chronological event timeline.
| Endpoint | Scope | What it does |
|---|---|---|
POST /v1/contacts | contacts:write | Create one contact. |
POST /v1/contacts/bulk | contacts:write | Create up to 1,000 contacts in one request. |
GET /v1/contacts | contacts:read | List, filter, search and count contacts, or check which emails you already have. |
GET /v1/contacts/:contact_id | contacts:read | One contact with its unified profile and recent events. |
PUT /v1/contacts/:contact_id | contacts:write | Change properties. |
DELETE /v1/contacts/:contact_id | contacts:write | Delete the contact. |
GET /v1/contacts/events/:contact_id | events:read | The contact’s event timeline. |
GET /v1/contacts/:contact_id/conversations | contacts:read | Email and chat threads, and meetings. |
GET /v1/contacts/:contact_id/meetings/:meeting_id/transcript | contacts:read | A recorded meeting’s transcript. |
GET /v1/contacts/:contact_id/email-threads | contacts:read | A compact list of the contact’s Gmail threads. |
GET, POST /v1/contacts/:contact_id/notes | contacts:read, contacts:write | List the contact’s notes, add one. |
PATCH, DELETE /v1/contacts/:contact_id/notes/:note_id | contacts:write | Change or delete one note. |
Related endpoints on other pages: tags on a contact, sending a contact an email, enriching contacts, and identifying a logged-in user from your server.
Create a Contact
Section titled “Create a Contact”POST /v1/contactsRequired scope: contacts:write
Without external_id, this creates a contact and an email that already exists is a 409. With external_id, your own id for the person, it is an upsert on that id, so the same request sent twice leaves one contact, unchanged by the second:
- A contact that has this
external_idis updated. - Else a contact that has no external id yet and holds the
emailtakes your id and is updated. If that contact is already keyed to another id, the answer is409withcode: "EXTERNAL_ID_CONFLICT"and itscontact_id, and nothing changes. - Else the contact is created.
An update merges what you send: a property you send replaces the stored value, and one you leave out keeps it. An email that another contact already holds is not written, and skipped_traits says so ("email": "email_belongs_to_contact:<contact_id>"); the other fields are.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
email | string | without external_id | Contact email address (must be unique). Optional with external_id. |
external_id | string | no | Your own id for the person, up to 100 characters, stored as external_contact_id. The same id identify takes. Placeholder values ("undefined", "null", …) are a 400. |
properties | object | no | Arbitrary key-value properties for the contact. lifecyclestage, if set, must be one of the lifecycle stages. |
visitor_id | string | string[] | no | Website visitor ID(s) from the events snippet to associate. A visitor already tied to another contact is left with it. |
company_id | string | no | Associate the contact with this company. An id that is not a company in your workspace is a 404. |
company | object | no | The company by your own key instead of its Pathbound id: { "external_id": "…" } (the id you gave it on Create a Company) or { "domain": "…" }. It only looks a company up: one that finds nothing is a 422 with code: "COMPANY_NOT_FOUND", and nothing is written. Not with company_id. |
A visitor is tied to a contact only through visitor_id: a sb_visitor_ids key in properties is ignored, here and on Update a Contact.
With external_id, a company you name replaces the contact’s company, and the company it had is removed from associations.companies: a user who moves to another account is no longer listed under the old one. Without a company, a contact on a corporate email domain is linked to the company that holds that domain, if there is one and the contact has no company yet.
Response — 201 Created
Section titled “Response — 201 Created”201 when the contact was created. A contact updated through its external_id is answered 200 OK in the same shape, with "created": false.
{ "status": "success", "message": "Contact created successfully", "contact_id": "665f1c2ab3d4e5f601234567", "created": true, "contact": { "contact_id": "665f1c2ab3d4e5f601234567", "_id": "665f1c2ab3d4e5f601234567", "properties": { "firstname": "Ada", "lastname": "Lovelace" }, "associations": { "companies": ["665f1c2ab3d4e5f6012345aa"] }, "first_seen_at": "2026-10-12T10:00:00.000Z", "segment_ids": [], "creation_source": "manual", "created_at": "2026-10-12T10:00:00.000Z", "updated_at": "2026-10-12T10:00:00.000Z", "notes": [], "tags": [] }, "timestamp": "2026-10-12T10:00:00.000Z"}_id is a transitional alias for contact_id and will be removed in a future release. Prefer contact_id in new integrations.
Without external_id, returns 409 Conflict if a contact with the same email already exists. The response carries that contact’s id as contact_id, so you can update it instead.
| Status | code | Meaning |
|---|---|---|
400 | INVALID_EXTERNAL_ID | external_id is empty, longer than 100 characters, or a placeholder value. |
400 | INVALID_LINK | company is not an object with exactly one of external_id and domain, or is sent beside company_id. |
409 | EXTERNAL_ID_CONFLICT | With external_id: the email belongs to a contact keyed to another id. contact_id is that contact. |
422 | COMPANY_NOT_FOUND | The company key finds no company in your workspace. |
Create Contacts in Bulk
Section titled “Create Contacts in Bulk”POST /v1/contacts/bulkRequired scope: contacts:write
Creates up to 1,000 contacts in one request. It is safe to send the same list twice: a contact whose email already exists is reported as existing and is not changed. An item with an external_id is an upsert on it, as on Create a Contact, and comes back created or updated. One bad item never fails the others.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
contacts | object[] | yes | 1 to 1,000 items. |
contacts[].email | string | without external_id | The contact’s email. |
contacts[].external_id | string | no | As in Create a Contact. |
contacts[].properties | object | no | As in Create a Contact. |
contacts[].company_id | string | no | A company to associate the contact with. |
contacts[].company | object | no | The company by your own key, as in Create a Contact. |
contacts[].tags | string[] | no | Tags to put on the contact. Applied to contacts the request creates or updates, not to an existing one. |
curl -X POST https://api.pathbound.ai/v1/contacts/bulk \ -H "Authorization: Bearer $PATHBOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contacts": [ { "email": "[email protected]", "properties": { "firstname": "Ada" }, "tags": ["webinar-oct"] }, { "email": "[email protected]", "properties": { "firstname": "Grace", "lifecyclestage": "lead" } }, { "email": "not-an-email" } ] }'Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "requested": 3, "created": 1, "updated": 0, "existing": 1, "errors": 1, "results": [ { "index": 0, "email": "[email protected]", "status": "existing", "id": "665f1c2ab3d4e5f601234567" }, { "index": 1, "email": "[email protected]", "status": "created", "id": "665f1c2ab3d4e5f601234568" }, { "index": 2, "email": "not-an-email", "status": "error", "error": "Valid email is required" } ], "timestamp": "2026-10-12T10:00:00.000Z"}results has one entry per item you sent, in the same order, and index is the item’s position in your array. An entry’s status is created, updated (an item with an external_id that found its contact), existing (an item without one whose email exists) or error, with the code of Create a Contact when it has one, and skipped_traits when a field was not written. The request is a 200 even when some items failed, so read errors and the entries with "status": "error". A 429 with code: "QUOTA_EXCEEDED" means the new contacts in the batch would take the workspace past its limit; an item that updates a contact through its external_id does not count toward it.
Visitor ids are not bound by this endpoint. To tie a visitor to a contact, use Create a Contact, Update a Contact or POST /v1/identify.
List Contacts
Section titled “List Contacts”GET /v1/contactsRequired scope: contacts:read
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number. |
limit | number | 10 | Results per page (1–100). |
fields | string | full | ids (the id only), summary (the id and key properties), full, or a comma-separated list of field names. See How much of each record comes back. Internal fields (tenant_id, user_id, __v, properties_provenance) cannot be requested. |
count_only | boolean | false | If true, return only { count } and no records. |
dry_run | boolean | false | If true, return what reading this page would add to your plan’s count, and no records. See Usage and metering. |
search | string | — | Free-text search across name, email, company, job title. |
sort_by | string | lastmodifieddate | email, firstname, lastname, company, jobtitle, createdate, lastmodifieddate, or an indexed source field written source.field. |
sort_dir | string | desc | Sort direction (asc or desc). |
status | string | — | Filter: enriched or not_enriched. |
lifecycle_stage | string | — | Filter by lifecycle stage (lead, prospect, mql, sql, opportunity, customer, evangelist, subscriber, other). Whole value, case-insensitive. |
lead_status | string | — | Filter by lead status (whole value, case-insensitive). |
country | string | — | Filter by country (whole value, case-insensitive). |
industry | string | — | Filter by industry (whole value, case-insensitive). |
city | string | — | Filter by city (whole value, case-insensitive). |
state | string | — | Filter by state/region (whole value, case-insensitive). |
job_title | string | — | Filter by job title (case-insensitive match). |
has_linkedin | boolean | — | Filter contacts with/without a LinkedIn profile URL. |
company_id | string | — | Filter by associated company ID (24 hex characters; any other value is a 400). |
external_id | string | — | Your own id for the person, the one identify stores as external_contact_id. Exact match, so zero or one contact. One id of up to 255 characters; a repeated parameter is a 400. |
created_after | string | — | Only contacts created after this ISO 8601 date. |
created_before | string | — | Only contacts created before this ISO 8601 date. |
tags_any | string | — | Comma-separated tags, up to 50. Contacts that carry at least one of them. |
tags_all | string | — | Comma-separated tags, up to 50. Contacts that carry every one of them. When both are sent, tags_all is used. |
data_sources | string | — | Comma-separated sources. Contacts that have data from at least one of them: hubspot, intercom, apollo, pipedrive, zoho, resend, zendesk, gmail, tldv, calendly, emailable, postgres (a connected database). An unknown name is a 400. |
segment | string | — | The id or handle of a saved segment. Only contacts in it. |
segments_any | string | — | Comma-separated segment ids or handles. Contacts in at least one of them. |
filters | string | — | Typed filters as a JSON array [{"field","op","value"}], up to 10, on properties or source fields (for example emailable.score, hubspot.hs_lead_status). GET /v1/schema?entity=contacts lists the fields available to your workspace. |
exclude_internal | boolean | false | Leave out your own team and test contacts, as defined in your workspace’s settings. |
email_verification_state | string | — | Deliverability filter: a comma-separated union of deliverable, undeliverable, risky, unknown, unverified (no verdict on file). deliverable gives a safe-to-email list; undeliverable,risky a suppression list. |
email_verified_after | string | — | Only contacts whose email verdict was recorded at or after this ISO 8601 date. |
email_verified_before | string | — | Only contacts whose email verdict was recorded at or before this ISO 8601 date (stale verdicts). Neither date bound can be combined with unverified (no verdict on file, so no verdict date); the request returns 400. |
has_event_type | string | — | Only contacts who have a tracked event of this type (e.g. button_click). |
has_event_text | string | — | Only contacts who have an event with this exact element text/label (data.text), e.g. Go!. |
has_event_text_contains | string | — | Only contacts who have an event whose element text/label contains this substring. |
has_event_url_contains | string | — | Only contacts who have an event whose URL contains this substring. |
has_event_class_contains | string | — | Only contacts who interacted with an element whose CSS class (data.class) contains this substring. This is the way to target elements without text or an id (icon buttons). Use a stable class token. |
has_event_element_id | string | — | Only contacts who have an event on the DOM element with this id (data.id). |
has_event_after | string | — | Only contacts whose matching event occurred at or after this ISO 8601 date. |
has_event_before | string | — | Only contacts whose matching event occurred at or before this ISO 8601 date. |
include_source_fields | boolean | false | With fields=summary: add source_fields, a selection of each connected source’s own values for the contact (for example hubspot.lifecyclestage). Personal fields are left out. |
include | string | — | Comma-separated list of optional fields to include. Currently supports provenance to add the per-field source/timestamp record (properties_provenance). Default omits provenance for response-size hygiene. |
emails | string | — | Switches the endpoint to checking which emails exist. No other parameter is applied, though each one sent is still validated. |
The has_event_* filters return the contacts who performed a matching tracked event. For example ?has_event_type=button_click&has_event_text=Go! lists the known people who clicked the Go! button. They compose with every other filter (&lifecycle_stage=lead), with count_only, and with pagination/sorting. Notes:
- Identified contacts only. Events are matched by their linked contact, so anonymous visitors never appear. To count distinct visitors (identified + anonymous), use
POST /v1/aggregatewithgroup_by=visitor_id. - Scope: using any
has_event_*filter requiresevents:readin addition tocontacts:read. - If an event filter matches an unusually large number of contacts, the request returns
400. Narrow it withhas_event_after/has_event_before.
Every listed contact carries email_verification_state, the deliverability verdict on file (deliverable, undeliverable, risky, unknown, or null when nobody has verified the address). It is the verdict whoever reached it: Emailable, or a data provider such as Hunter that verified the address it found. emailable: { state, score } | null adds Emailable’s own detail and is null for an address Emailable did not check. The email_verification_state query parameter filters on the verdict server-side, so ?email_verification_state=deliverable&count_only=true answers “how many contacts are safe to email” without paging any records. The finer verdict fields (score, reason, role, free, disposable, accept_all, smtp_provider) are available through filters as emailable.<field> once Emailable is connected.
Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "contacts": [ { "contact_id": "665f1c2ab3d4e5f601234567", "_id": "665f1c2ab3d4e5f601234567", "associations": { "companies": ["665f1c2ab3d4e5f6012345aa"] }, "first_seen_at": "2026-09-15T10:00:00.000Z", "created_at": "2026-09-15T10:00:00.000Z", "updated_at": "2026-10-12T10:00:00.000Z", "tags": ["webinar-oct"], "emailable": { "state": "deliverable", "score": 97 } } ], "pagination": { "page": 1, "limit": 10, "total_pages": 10, "total_items": 100 }, "timestamp": "2026-10-12T10:00:00.000Z"}_id is a transitional alias for contact_id and will be removed in a future release.
With fields=summary each row is flat:
{ "contact_id": "665f1c2ab3d4e5f601234567", "_id": "665f1c2ab3d4e5f601234567", "firstname": "Ada", "lastname": "Lovelace", "company": "Analytical Engines", "lifecyclestage": "customer", "last_activity_at": "2026-10-12T10:00:00.000Z", "emailable": { "state": "deliverable", "score": 97 }, "email_verification_state": "deliverable", "tags": ["webinar-oct"]}With fields=ids each row is { "contact_id", "_id" } and the page does not count against your plan. A page in any other shape counts every contact on it as read. To walk a large list cheaply, filter with fields=ids and read what you need through the batch API.
For a “how many” question, prefer count_only=true.
Check which emails exist
Section titled “Check which emails exist”Pass emails, a comma-separated list of up to 50 addresses, to learn which of them are already contacts. This is the cheap way to avoid creating duplicates before an import. It is free, and it matches the whole address exactly after normalizing it, unlike search.
curl -G https://api.pathbound.ai/v1/contacts \ -H "Authorization: Bearer $PATHBOUND_API_KEY" \{ "status": "success", "mode": "exists", "requested": 2, "found": 1, "results": [ ], "timestamp": "2026-10-12T10:00:00.000Z"}Addresses past the 50th are not checked, and the response then carries "truncated": true.
Get a Contact
Section titled “Get a Contact”GET /v1/contacts/:contact_idRequired scope: contacts:read
Returns the contact along with its unified profile and (optionally) recent events.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
include_events | boolean | true | If false, omit the events array and total_events. |
events_limit | number | 20 | Max events to include (1–50). Ignored when include_events=false. |
include | string | — | Comma-separated list of optional fields to include. Supports provenance (per-field provenance on the base contact record) and raw (adds unified_profile.raw_data, the un-deduplicated per-source field dumps; large, omitted by default). |
dry_run | boolean | false | If true, return whether reading this contact would add to your plan’s count, and no record. |
Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "contact": { "contact_id": "665f1c2ab3d4e5f601234567", "_id": "665f1c2ab3d4e5f601234567", "associations": { "companies": ["665f1c2ab3d4e5f6012345aa"] }, "_summary": "Ada Lovelace | CTO | at Analytical Engines | London | Stage: customer", "created_at": "2026-09-15T10:00:00.000Z", "updated_at": "2026-10-12T12:00:00.000Z" }, "unified_profile": { "_id": "665f1c2ab3d4e5f601234567", "source_count": 3, "sources": { "internal": { "id": "665f1c2ab3d4e5f601234567", "updated_at": "...", "first_seen_at": "..." }, "hubspot": { "id": "12345", "synced_at": "...", "first_seen_at": "..." }, "apollo": { "id": "ap_678", "enriched_at": "..." } }, "profile": { "email": { "unique_values": [ { "value": "[email protected]", "sources": [{ "source": "hubspot", "updated_at": "..." }, { "source": "apollo", "updated_at": "..." }] } ], "all_sources": ["hubspot", "apollo"], "has_conflict": false }, "jobtitle": { "unique_values": [ { "value": "CTO", "sources": [{ "source": "hubspot", "updated_at": "..." }] }, { "value": "Chief Technology Officer", "sources": [{ "source": "apollo", "updated_at": "..." }] } ], "all_sources": ["hubspot", "apollo"], "has_conflict": true } }, "created_at": "...", "updated_at": "...", "enriched_at": "..." }, "events": [ { "event_id": "665f1c2ab3d4e5f6012345e1", "_id": "665f1c2ab3d4e5f6012345e1", "event": "page_view", "timestamp": "...", "url": "...", "title": "..." } ], "total_events": 42, "timestamp": "2026-10-12T12:00:00.000Z"}_summary is a human-readable one-liner the backend stitches together from the contact’s name, title, company, industry, location, and lifecycle stage. Parts are joined with | and prefixed where applicable (at <company>, Stage: <stage>). Convenient for list views, but not a stable contract, so don’t parse it.
unified_profile is the full deduplicated cross-source profile. Each profile field is a multi-source object: unique_values holds every distinct value found across sources (deduplicated), each annotated with the sources that supplied it; all_sources lists every source that had any value for the field; and has_conflict is true when sources disagree (more than one distinct value). sources is a per-source map (presence, external IDs, and sync/enrichment timestamps), and source_count is how many sources contributed. Pass ?include=raw to additionally get raw_data, the verbatim, un-deduplicated per-source field dumps. events is a compact projection of the contact’s most recent events and is omitted (along with total_events) when the request is made with ?include_events=false.
When PII redaction is enabled for an AI-agent (MCP) caller, the personal values inside unified_profile.profile are masked (the field and its source attribution remain visible) and raw_data is never returned, even with ?include=raw. An API key is never redacted.
Update a Contact
Section titled “Update a Contact”PUT /v1/contacts/:contact_idRequired scope: contacts:write
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
properties | object | yes | Properties to update or add. They are merged into the contact: a property you do not send keeps its value. Send {} to change only the fields below. |
visitor_id | string | string[] | no | Visitor ID(s) to associate. |
company_id | string | no | A company to associate the contact with. An id that is not a company in your workspace is a 404. |
To change a contact’s lifecycle stage, set properties.lifecyclestage to one of the allowed values.
The response is the updated contact, in the shape of Create a Contact.
Lifecycle stages
Section titled “Lifecycle stages”lead, prospect, mql, sql, opportunity, customer, evangelist, subscriber, other.
Delete a Contact
Section titled “Delete a Contact”DELETE /v1/contacts/:contact_idRequired scope: contacts:write
Permanently removes the contact and its notes. The contact’s events are kept. This cannot be undone.
An AI agent connected through the MCP server cannot call this endpoint: it is answered 403 with the code AGENT_DELETE_FORBIDDEN. An API key can.
Get Contact Events
Section titled “Get Contact Events”GET /v1/contacts/events/:contact_idRequired scope: events:read
Returns the events associated with a contact, newest first by default. For aggregate event queries (across many contacts), use the Events API directly.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | Events to return (1–200). |
offset | number | 0 | Events to skip. Page with offset and limit. |
event_type | string | — | Filter by event type. |
date_after | string | — | Only events at or after this ISO 8601 date. |
date_before | string | — | Only events at or before this ISO 8601 date. |
sort_dir | string | desc | Sort direction (asc or desc). |
fields | string | — | summary for a compact projection (event_id, _id, event, timestamp, url, contact_id); omit for full. |
count_only | boolean | false | If true, return only { count }: how many of the contact’s events match. Free. |
Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "events": [ { "event_id": "665f1c2ab3d4e5f6012345e1", "_id": "665f1c2ab3d4e5f6012345e1", "event": "page_view", "timestamp": "2026-10-12T11:23:45.000Z", "url": "https://example.com/pricing", "domain": "example.com", "data": { "title": "Pricing — Example", "path": "/pricing" } } ], "contact_id": "665f1c2ab3d4e5f601234567", "total_items": 212, "returned": 50, "timestamp": "2026-10-12T12:00:00.000Z"}total_items is how many events match in all, and returned how many are in this response. Ask for the next page with offset raised by limit until you have total_items.
Get Contact Conversations
Section titled “Get Contact Conversations”GET /v1/contacts/:contact_id/conversationsRequired scope: contacts:read
Returns the contact’s conversation history, one block per channel: email (Gmail/SMTP), intercom, hubspot (logged 1:1 emails), instantly (cold-email messages), and meetings (tl;dv recordings and Calendly bookings). Each block has its own conversations[] and pagination. Blocks for integrations that aren’t connected come back empty.
The same history across everyone at a company is GET /v1/companies/:company_id/conversations.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
source | string | all | One channel: email, intercom, hubspot, instantly, meetings, or all. |
include_messages | boolean | false | Include message bodies (email/HubSpot/Instantly truncated to 5,000 characters per message, Intercom parts to 2,000; the last 20 messages per email thread). For meetings, adds tl;dv’s AI notes or Calendly’s booking-form answers. Never the transcript. |
page | number | 1 | Page number. Each channel pages independently. |
limit | number | 20 | Threads per channel per page (1–50). |
sort_dir | string | desc | asc or desc, by last-message date (meetings: by meeting date). |
date_after | string | — | ISO 8601. Only threads with activity (or meetings held) after this date. |
date_before | string | — | ISO 8601. Only threads with activity (or meetings held) before this date. |
With include_messages=true, an Intercom conversation’s parts start with its opening message (part_type: "source", dated with the conversation’s created_at), followed by the conversation parts in order.
A tl;dv meeting in the meetings block reports has_transcript, transcript_segment_count, and has_notes. Read the transcript itself with Get Meeting Transcript. To read notes from many meetings at once, use POST /v1/meetings/query.
Get Meeting Transcript
Section titled “Get Meeting Transcript”GET /v1/contacts/:contact_id/meetings/:meeting_id/transcriptRequired scope: contacts:read
Returns one page of a tl;dv recording’s transcript. meeting_id is the _id of a meeting in the meetings block of the conversations endpoint, and contact_id must be a contact who was on the call. Any other combination returns 404. Calendly bookings have no transcript.
Pages are cut by size, not by segment count. To read the whole transcript, pass each response’s next_offset back as offset until next_offset is null.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
offset | number | 0 | Segment index to start from. |
max_chars | number | 8000 | Page size in characters (1,000–50,000). |
Response
Section titled “Response”{ "status": "success", "contact_id": "665f1c2ab3d4e5f601234567", "meeting": { "_id": "6650f1c2e4b0a1b2c3d4e5f6", "name": "QBR — Acme", "happened_at": "2026-09-22T15:00:00.000Z", "duration_seconds": 2280 }, "transcript": { "segments": [ { "speaker": "Dana Lee", "start_time": 0, "text": "Thanks for making time today." }, { "speaker": "Sam Ortiz", "start_time": 4, "text": "Happy to. Let's start with the security review." } ], "offset": 0, "returned": 2, "total_segments": 412, "next_offset": 2 }}start_time is seconds from the start of the recording. A meeting with no transcript returns an empty page with unavailable_reason: not_available (tl;dv hasn’t produced one yet) or access_denied (tl;dv refused this connection access to the recording).
List Contact Email Threads
Section titled “List Contact Email Threads”GET /v1/contacts/:contact_id/email-threadsRequired scope: contacts:read
Returns a compact list of the contact’s Gmail threads, in threads. count is the number of threads in this response, at most limit: it is not the contact’s total, and no total is returned. Each entry names the thread and its last message, which is what you need to reply inside a thread.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 10 | Threads to return (1–50). |
Contact Notes
Section titled “Contact Notes”Each contact has a list of free-form notes. Notes are written directly (no approval flow) and are visible to anyone with contacts:read.
List Notes
Section titled “List Notes”GET /v1/contacts/:contact_id/notesRequired scope: contacts:read
Create a Note
Section titled “Create a Note”POST /v1/contacts/:contact_id/notesRequired scope: contacts:write
| Field | Type | Required | Description |
|---|---|---|---|
content | string | yes | Note body (markdown), up to 10,000 characters. |
Update a Note
Section titled “Update a Note”PATCH /v1/contacts/:contact_id/notes/:note_idRequired scope: contacts:write
Takes the same body as Create a Note and replaces the note’s text.
Delete a Note
Section titled “Delete a Note”DELETE /v1/contacts/:contact_id/notes/:note_idRequired scope: contacts:write