Skip to content
Pathbound DOCS

Companies API

Companies represent organizations in your Pathbound workspace, identified by their domain. Contacts roll up to companies for account-level intelligence.

EndpointScopeWhat it does
POST /v1/companiescompanies:writeCreate one company.
POST /v1/companies/bulkcompanies:writeCreate up to 1,000 companies in one request.
GET /v1/companiescompanies:readList, filter, search and count companies, or check which domains you already have.
GET /v1/companies/:company_idcompanies:readOne company with its unified profile and contacts.
PUT /v1/companies/:company_idcompanies:writeChange properties.
DELETE /v1/companies/:company_idcompanies:writeDelete the company.
GET /v1/companies/:company_id/conversationscompanies:read and contacts:readEmail and chat threads and meetings with everyone at the company.
GET, POST /v1/companies/:company_id/notescompanies:read, companies:writeList the company’s notes, add one.
PATCH, DELETE /v1/companies/:company_id/notes/:note_idcompanies:writeChange or delete one note.

Related endpoints on other pages: tags on a company, deals, and enriching companies and finding people at them.

POST /v1/companies

Required scope: companies:write

Without external_id, this creates a company and a domain that already exists is a 409. With external_id, your own id for the account, it is an upsert on that id, so the same request sent twice leaves one company, unchanged by the second, and two sent at the same moment make one:

  1. A company that has this external_id is updated.
  2. Else a company that has no external id yet and holds the domain (one your CRM brought in, say) takes your id and is updated.
  3. Else the company is created.

An update merges what you send: a property you send replaces the stored value, and one you leave out keeps it. A plan change is this call again with the new values.

With external_id the domain is optional, and a domain that cannot be stored is left off while the company is still written; skipped_traits.domain says why:

skipped_traits.domainWhy
free_mail_domainThe domain is a free email provider’s, such as gmail.com: a user who signs up with a personal address still gets an account, without a domain.
invalid_domainThe domain is not a host name (no dot).
domain_belongs_to_company:<company_id>Another company holds the domain, for example a second account at the same firm. The domain stays with that company.
FieldTypeRequiredDescription
domainstringwithout external_idCompany domain (must be unique). It is stored normalized: https://www.Example.com/about is example.com. A domain inside properties is ignored.
external_idstringnoYour own id for the account, up to 100 characters, stored as external_company_id. Placeholder values ("undefined", "null", …) are a 400 with code: "INVALID_EXTERNAL_ID".
propertiesobjectnoArbitrary key-value properties. lifecyclestage, if set, must be one of lead, prospect, mql, sql, opportunity, customer, evangelist, subscriber, other.

201 when the company was created. A company updated through its external_id is answered 200 OK in the same shape, with "created": false.

{
"status": "success",
"message": "Company created successfully",
"company_id": "665f1c2ab3d4e5f6012345aa",
"created": true,
"company": {
"company_id": "665f1c2ab3d4e5f6012345aa",
"_id": "665f1c2ab3d4e5f6012345aa",
"properties": {
"domain": "example.com",
"name": "Example Corp"
},
"created_at": "2026-09-15T10:00:00.000Z",
"updated_at": "2026-09-15T10:00:00.000Z"
}
}

_id is a transitional alias for company_id and will be removed in a future release. Prefer company_id in new integrations.

Without external_id, returns 409 Conflict if a company with the same domain already exists, with its id as company_id.

Without external_id, returns 400 with a code when the domain cannot name a company, and nothing is created:

codeWhen
INVALID_DOMAINThe domain is not a host name: no dot, as in acme.
FREE_MAIL_DOMAINThe domain is a free email provider’s, such as gmail.com or outlook.com: it is where a person’s mail is, not a company.
{
"status": "error",
"code": "FREE_MAIL_DOMAIN",
"error": "gmail.com is a free email provider, not a company's domain",
"timestamp": "2026-10-22T10:00:00.000Z"
}

POST /v1/companies/bulk

Required scope: companies:write

Creates up to 1,000 companies in one request. It is safe to send the same list twice: a company whose domain 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 Company, and comes back created or updated. One bad item never fails the others.

FieldTypeRequiredDescription
companiesobject[]yes1 to 1,000 items.
companies[].domainstringwithout external_idThe company’s domain.
companies[].external_idstringnoAs in Create a Company.
companies[].propertiesobjectnoAs in Create a Company.
companies[].tagsstring[]noTags to put on the company. Applied to companies the request creates or updates, not to an existing one.
{
"status": "success",
"requested": 2,
"created": 1,
"updated": 0,
"existing": 1,
"errors": 0,
"results": [
{ "index": 0, "domain": "example.com", "status": "existing", "id": "665f1c2ab3d4e5f6012345aa" },
{ "index": 1, "domain": "analyticalengines.example", "status": "created", "id": "665f1c2ab3d4e5f6012345ab" }
],
"timestamp": "2026-10-12T10:00:00.000Z"
}

results has one entry per item you sent, in the same order. An entry’s status is created, updated (an item with an external_id that found its company), existing (an item without one whose domain exists) or error. An item that could not be written has an error text, and the code of Create a Company when it has one (INVALID_DOMAIN, FREE_MAIL_DOMAIN, INVALID_EXTERNAL_ID); skipped_traits appears when a domain was left off. The request is a 200 even when some items failed, so read errors. A 429 with code: "QUOTA_EXCEEDED" means the new companies in the batch would take the workspace past its limit; an item that updates a company through its external_id does not count toward it.


GET /v1/companies

Required scope: companies:read

ParameterTypeDefaultDescription
pagenumber1Page number.
limitnumber10Results per page (1–100).
fieldsstringfullids (the id only), summary (company_id, id, name, domain, industry, last_activity_at, tags) or full. See How much of each record comes back.
count_onlybooleanfalseIf true, return only { count } and no records.
dry_runbooleanfalseIf true, return what reading this page would add to your plan’s count, and no records. See Usage and metering.
searchstring—Free-text search by name, domain, industry.
sort_bystringlastmodifieddatedomain, name, createdate, lastmodifieddate, or an indexed source field written source.field (for example apollo.employee_count).
sort_dirstringdescSort direction (asc or desc).
industrystring—Filter by industry (whole value, case-insensitive).
countrystring—Filter by country (whole value, case-insensitive).
external_idstring—Your own id for the account, the one Create a Company stores as external_company_id. Exact match, so zero or one company. One id of up to 255 characters; a repeated parameter is a 400.
tags_anystring—Comma-separated tags, up to 50. Companies that carry at least one of them.
tags_allstring—Comma-separated tags, up to 50. Companies that carry every one of them. When both are sent, tags_all is used.
data_sourcesstring—Comma-separated sources. Companies that have data from at least one of them: hubspot, intercom, apollo, pipedrive, zendesk, zoho, tldv, calendly, postgres (a connected database). An unknown name is a 400.
segmentstring—The id or handle of a saved segment of companies. Only companies in it.
segments_anystring—Comma-separated segment ids or handles. Companies in at least one of them.
filtersstring—Typed filters as a JSON array [{"field","op","value"}], up to 10, on properties or source fields (for example apollo.employee_count, hubspot.numberofemployees). GET /v1/schema?entity=companies lists the fields available to your workspace.
exclude_internalbooleanfalseLeave out your own company and test companies, as defined in your workspace’s settings.
include_source_fieldsbooleanfalseWith fields=summary: add source_fields, a selection of each connected source’s own firmographics for the company.
includestring—Comma-separated list of optional fields to include. Currently supports provenance to add the per-field source/timestamp record (properties_provenance).
domainsstring—Switches the endpoint to checking which domains exist. No other parameter is applied, though each one sent is still validated.
{
"status": "success",
"companies": [
{
"company_id": "665f1c2ab3d4e5f6012345aa",
"_id": "665f1c2ab3d4e5f6012345aa",
"properties": { "domain": "example.com", "name": "Example Corp", "industry": "Software" },
"created_at": "2026-09-15T10:00:00.000Z",
"updated_at": "2026-10-12T12:00:00.000Z",
"tags": ["enterprise"]
}
],
"pagination": {
"page": 1,
"limit": 10,
"total_pages": 3,
"total_items": 25
},
"timestamp": "2026-10-12T12:00:00.000Z"
}

_id is a transitional alias for company_id and will be removed in a future release.

With fields=ids each row is { "company_id", "id" } and the page does not count against your plan. A page in any other shape counts every company on it as read. To walk a large list cheaply, filter with fields=ids and read what you need through the batch API.

Pass domains, a comma-separated list of up to 50 domains, to learn which of them are already companies. It is free, and it matches the whole domain exactly after normalizing it, unlike search.

Terminal window
curl -G https://api.pathbound.ai/v1/companies \
-H "Authorization: Bearer $PATHBOUND_API_KEY" \
--data-urlencode "domains=example.com,unknown.example"
{
"status": "success",
"mode": "exists",
"requested": 2,
"found": 1,
"results": [
{ "domain": "example.com", "exists": true, "id": "665f1c2ab3d4e5f6012345aa" },
{ "domain": "unknown.example", "exists": false, "id": null }
],
"timestamp": "2026-10-12T12:00:00.000Z"
}

Domains past the 50th are not checked, and the response then carries "truncated": true.


GET /v1/companies/:company_id

Required scope: companies:read

Returns the company along with its full deduplicated cross-source unified_profile and its associated contacts.

ParameterTypeDefaultDescription
include_contactsbooleantrueIf false, omit the contacts array from the response.
contacts_limitnumber10How many associated contacts to return (1–25).
includestring—Comma-separated list of optional fields to include. Supports provenance (per-field provenance on the base company record) and raw (adds unified_profile.raw_data, the un-deduplicated per-source field dumps; large, omitted by default).
{
"status": "success",
"company": {
"company_id": "665f1c2ab3d4e5f6012345aa",
"_id": "665f1c2ab3d4e5f6012345aa",
"properties": {
"domain": "example.com",
"name": "Example Corp",
"industry": "Software",
"employee_count": 250,
"lifecyclestage": "customer"
},
"created_at": "2026-09-15T10:00:00.000Z"
},
"unified_profile": {
"_id": "665f1c2ab3d4e5f6012345aa",
"source_count": 2,
"sources": {
"internal": { "id": "665f1c2ab3d4e5f6012345aa", "updated_at": "...", "first_seen_at": "..." },
"hubspot": { "id": "98765", "synced_at": "...", "first_seen_at": "..." }
},
"profile": {
"name": {
"unique_values": [
{ "value": "Example Corp", "sources": [{ "source": "internal", "updated_at": "..." }, { "source": "hubspot", "updated_at": "..." }] }
],
"all_sources": ["internal", "hubspot"],
"has_conflict": false
},
"numberofemployees": {
"unique_values": [
{ "value": 250, "sources": [{ "source": "internal", "updated_at": "..." }] },
{ "value": 300, "sources": [{ "source": "hubspot", "updated_at": "..." }] }
],
"all_sources": ["internal", "hubspot"],
"has_conflict": true
}
},
"created_at": "...",
"updated_at": "..."
},
"contacts": [
{
"id": "665f1c2ab3d4e5f601234567",
"email": "[email protected]",
"firstname": "Ada",
"lastname": "Lovelace",
"job_title": "CTO",
"lifecycle_stage": "customer"
}
],
"total_contacts": 12
}

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; and has_conflict is true when sources disagree. sources is a per-source map (presence, external IDs, sync/enrichment timestamps), and source_count is how many sources contributed. Pass ?include=raw for raw_data (the verbatim, un-deduplicated per-source dumps). Company firmographics are not PII and are never masked.

The embedded contacts array is a compact projection (intentionally uses id rather than the full contact serializer). For the full contact shape, fetch the contact directly via GET /v1/contacts/:contact_id.


PUT /v1/companies/:company_id

Required scope: companies:write

FieldTypeRequiredDescription
propertiesobjectyesProperties to update or add. They are merged into the company: a property you do not send keeps its value.
{
"status": "success",
"company": {
"company_id": "665f1c2ab3d4e5f6012345aa",
"_id": "665f1c2ab3d4e5f6012345aa",
"properties": { "domain": "example.com", "name": "Example Corp", "industry": "Software" },
"updated_at": "2026-10-12T12:00:00.000Z"
}
}

DELETE /v1/companies/:company_id

Required scope: companies:write

Permanently removes the company and its notes. Its contacts are not deleted. 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 /v1/companies/:company_id/conversations

Required scopes: companies:read and contacts:read

Returns the conversation history of everyone at the company in one response: the same channel blocks as Get Contact Conversations, each holding the threads of all the company’s contacts. It takes the same query parameters (source, include_messages, page, limit, sort_dir, date_after, date_before).

The response also carries company_id and contact_count, the number of contacts whose history was read. Use it to answer “what have we discussed with this account” without walking the contacts one by one.


Each company can have free-form notes attached, just like contacts.

GET /v1/companies/:company_id/notes

Required scope: companies:read

POST /v1/companies/:company_id/notes

Required scope: companies:write

FieldTypeRequiredDescription
contentstringyesNote body (markdown), up to 10,000 characters.
PATCH /v1/companies/:company_id/notes/:note_id

Required scope: companies:write

DELETE /v1/companies/:company_id/notes/:note_id

Required scope: companies:write