Companies API
Companies represent organizations in your Pathbound workspace, identified by their domain. Contacts roll up to companies for account-level intelligence.
| Endpoint | Scope | What it does |
|---|---|---|
POST /v1/companies | companies:write | Create one company. |
POST /v1/companies/bulk | companies:write | Create up to 1,000 companies in one request. |
GET /v1/companies | companies:read | List, filter, search and count companies, or check which domains you already have. |
GET /v1/companies/:company_id | companies:read | One company with its unified profile and contacts. |
PUT /v1/companies/:company_id | companies:write | Change properties. |
DELETE /v1/companies/:company_id | companies:write | Delete the company. |
GET /v1/companies/:company_id/conversations | companies:read and contacts:read | Email and chat threads and meetings with everyone at the company. |
GET, POST /v1/companies/:company_id/notes | companies:read, companies:write | List the company’s notes, add one. |
PATCH, DELETE /v1/companies/:company_id/notes/:note_id | companies:write | Change or delete one note. |
Related endpoints on other pages: tags on a company, deals, and enriching companies and finding people at them.
Create a Company
Section titled “Create a Company”POST /v1/companiesRequired 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:
- A company that has this
external_idis updated. - 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. - 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.domain | Why |
|---|---|
free_mail_domain | The 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_domain | The 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. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
domain | string | without external_id | Company domain (must be unique). It is stored normalized: https://www.Example.com/about is example.com. A domain inside properties is ignored. |
external_id | string | no | Your 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". |
properties | object | no | Arbitrary key-value properties. lifecyclestage, if set, must be one of lead, prospect, mql, sql, opportunity, customer, evangelist, subscriber, other. |
Response — 201 Created
Section titled “Response — 201 Created”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:
code | When |
|---|---|
INVALID_DOMAIN | The domain is not a host name: no dot, as in acme. |
FREE_MAIL_DOMAIN | The 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"}Create Companies in Bulk
Section titled “Create Companies in Bulk”POST /v1/companies/bulkRequired 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.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
companies | object[] | yes | 1 to 1,000 items. |
companies[].domain | string | without external_id | The company’s domain. |
companies[].external_id | string | no | As in Create a Company. |
companies[].properties | object | no | As in Create a Company. |
companies[].tags | string[] | no | Tags to put on the company. Applied to companies the request creates or updates, not to an existing one. |
Response — 200 OK
Section titled “Response — 200 OK”{ "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.
List Companies
Section titled “List Companies”GET /v1/companiesRequired scope: companies: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 (company_id, id, name, domain, industry, last_activity_at, tags) or full. See How much of each record comes back. |
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 by name, domain, industry. |
sort_by | string | lastmodifieddate | domain, name, createdate, lastmodifieddate, or an indexed source field written source.field (for example apollo.employee_count). |
sort_dir | string | desc | Sort direction (asc or desc). |
industry | string | — | Filter by industry (whole value, case-insensitive). |
country | string | — | Filter by country (whole value, case-insensitive). |
external_id | string | — | 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_any | string | — | Comma-separated tags, up to 50. Companies that carry at least one of them. |
tags_all | string | — | Comma-separated tags, up to 50. Companies that carry every one of them. When both are sent, tags_all is used. |
data_sources | string | — | 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. |
segment | string | — | The id or handle of a saved segment of companies. Only companies in it. |
segments_any | string | — | Comma-separated segment ids or handles. Companies 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 apollo.employee_count, hubspot.numberofemployees). GET /v1/schema?entity=companies lists the fields available to your workspace. |
exclude_internal | boolean | false | Leave out your own company and test companies, as defined in your workspace’s settings. |
include_source_fields | boolean | false | With fields=summary: add source_fields, a selection of each connected source’s own firmographics for the company. |
include | string | — | Comma-separated list of optional fields to include. Currently supports provenance to add the per-field source/timestamp record (properties_provenance). |
domains | string | — | Switches the endpoint to checking which domains exist. No other parameter is applied, though each one sent is still validated. |
Response — 200 OK
Section titled “Response — 200 OK”{ "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.
Check which domains exist
Section titled “Check which domains exist”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.
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 a Company
Section titled “Get a Company”GET /v1/companies/:company_idRequired scope: companies:read
Returns the company along with its full deduplicated cross-source unified_profile and its associated contacts.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
include_contacts | boolean | true | If false, omit the contacts array from the response. |
contacts_limit | number | 10 | How many associated contacts to return (1–25). |
include | string | — | 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). |
Response — 200 OK
Section titled “Response — 200 OK”{ "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", "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.
Update a Company
Section titled “Update a Company”PUT /v1/companies/:company_idRequired scope: companies:write
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
properties | object | yes | Properties to update or add. They are merged into the company: a property you do not send keeps its value. |
Response — 200 OK
Section titled “Response — 200 OK”{ "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 a Company
Section titled “Delete a Company”DELETE /v1/companies/:company_idRequired 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 Company Conversations
Section titled “Get Company Conversations”GET /v1/companies/:company_id/conversationsRequired 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.
Company Notes
Section titled “Company Notes”Each company can have free-form notes attached, just like contacts.
List Notes
Section titled “List Notes”GET /v1/companies/:company_id/notesRequired scope: companies:read
Create a Note
Section titled “Create a Note”POST /v1/companies/:company_id/notesRequired scope: companies: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/companies/:company_id/notes/:note_idRequired scope: companies:write
Delete a Note
Section titled “Delete a Note”DELETE /v1/companies/:company_id/notes/:note_idRequired scope: companies:write