---
title: "Search companies"
method: POST
path: "/api/companies/search"
tags: ["companies"]
---

# Search companies

`POST /api/companies/search`

## Request

- Base URL: `https://beta.getsales.io/playtables/v1`
- URL: `https://beta.getsales.io/playtables/v1/api/companies/search`
- Auth: HTTP bearer and API key in header `Team-SID`

Companies from the Elasticsearch projection: every company tab is this tool with filter playtable_sids {in: [<table>]}. Filter coverage (domain is_null), tags, status, signals; q is free text over name, domain, website and LinkedIn. Firmographics are cell-value view conditions, not entity filters. Includes: field_values (custom columns), people_counts, signals; page_size 0 = counts only, counts.groups.signals per signal. Fresh writes lag ES by seconds: read-your-writes is get_company.

Contract:
- MCP tool `search_companies`, registry package `mcp.playtables/companies`, mount `playtables.people`.
- Operation `search`, response envelope `search`.
- Flags: read only.

## Request body

- SearchCompaniesRequest: Request body of `search_companies`.
  - `filter` object: Field filters, ANDed: {field: {eq | ne | in | nin | gte | lte | gt | lt | is_null}}; q inside it is free text. Omit for every row.
    - `sid` object: Prefix-validated cr_co_ sid(s).
      - `eq` string: Company sid (cr_co_…).
      - `in` string[]
    - `playtable_sids` object: THE table context - every company tab passes { in: [<table>] }; array-overlap. nin answers the complement, memberless rows included.
      - `in` string[]
      - `nin` string[]
    - `pipeline_stage_category` object: The account's funnel bucket (one of seven), expanded on the server into the team's company stage sids of those categories: ask by name, never resolve the stages yourself. eq approaching = someone was invited or written to and nobody answered.
      - `eq` 'cold' | 'approaching' | 'engaging' | 'positive' | 'nurture' | 'negative' | 'disqualified'
      - `in` string[]
    - `signals` object[]: ES-only. Named signals of the account, all must hold: its own visit and the account names rolled up over the people to work (disqualified or stoplisted people do not count). Never compose the roll-ups yourself. A person's or a thread's name is a 422.
      - `key` 'awaiting_reply' | 'opened_not_replied' | 'clicked_not_replied' | 'multi_opened' | 'accepted_not_replied' | 'note_read_not_replied' | 'reacted_not_replied' | 'invited_us' | 'followed_us' | 'lead_messaged_first' | 'unanswered_inbound' | 'draft_pending' | 'escalated_open' | 'task_overdue' | 'went_quiet' | 'reactivated' | 'invite_pending' | 'connection_removed' | 'blocked_us' | 'deleted_message' | 'bounced' | 'bounced_hard' | 'complained' | 'booking_link_sent' | 'objective_reached' | 'viewed_our_profile' | 'visited_our_site' | 'visited_not_replied' | 'visited_key_page' | 'multi_visited' | 'submitted_form' | 'company_visited_our_site' | 'no_people' | 'thin_coverage' | 'nobody_reached' | 'no_engagement' | 'no_reply' | 'exhausted' | 'dormant' | 'dm_missing', required: A catalog signal name. Thread and person names: awaiting_reply (our message is the last), opened_not_replied and clicked_not_replied (our email opened or its link clicked, no reply), multi_opened (3+ opens), accepted_not_replied (invite accepted, nothing after), note_read_not_replied (the accept delivered our invite note, no reply), reacted_not_replied (an emoji on our message, no reply), invited_us (their invitation to us, unanswered), followed_us, lead_messaged_first (an inbound thread), unanswered_inbound, draft_pending (an AI draft awaits approval), escalated_open (handed to a person, task open), task_overdue, went_quiet (they had replied, then nothing), reactivated (an inbound after 30+ quiet days), invite_pending (our invite out, neither accepted nor withdrawn), connection_removed, blocked_us, deleted_message, bounced, bounced_hard (the address is gone), complained, booking_link_sent, objective_reached (the live dialog landed its goal), viewed_our_profile. Website names: visited_our_site, visited_not_replied, visited_key_page (a page the team marked key), multi_visited, submitted_form, company_visited_our_site (someone from the company, nobody recognized; a person's and a company's). Account names, companies only (dormant also a person's): no_people (nobody to work), thin_coverage (under 3 people to work), nobody_reached, no_engagement (touched, nothing came back), no_reply (touched, no reply), exhausted (everyone nurture or negative), dormant (touched once, quiet since), dm_missing (a Wrong Decision Maker named, no positive person left). Tracking facts are private: never tell the lead (submitted_form excepted).
      - `age_days` integer: At least this many days on the signal's clock (0 = any). Defaults when omitted: 1 visited_not_replied; 3 awaiting_reply and the *_not_replied names; 14 went_quiet, invite_pending; 30 no_engagement, no_reply; 180 dormant; the rest any age.
    - `data_source_sid` object: Import provenance (ds_sc_…).
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `status` object
      - `eq` 'ok' | 'failed_validation' | 'duplicate' | 'stoplisted'
      - `ne` 'ok' | 'failed_validation' | 'duplicate' | 'stoplisted'
      - `in` string[]
      - `nin` string[]
    - `duplicate_of_sid` object: The copies of a primary: { eq: <primary sid> }. is_null:true = rows that are not flagged duplicates.
      - `eq` string: Company sid (cr_co_…).
      - `in` string[]
      - `is_null` boolean
    - `tag_sids` object: Array-overlap (any of); cr_tg_ sids - resolve labels via tags. nin answers the complement, untagged rows included.
      - `in` string[]
      - `nin` string[]
    - `ln_id` object: Exact LinkedIn company id.
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `ln_nickname` object: Exact page slug; fuzzy matching goes through q.
      - `eq` string
      - `is_null` boolean
    - `domain` object: Lowercased apex; is_null = enrichment gap.
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `name` object: Exact match; substring goes through q.
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `q` string: Free text over name, domain, website, ln_nickname (ES full-text).
    - `created_at` object: ISO 8601 range; combine bounds in one object.
      - `gte` string
      - `lte` string
      - `gt` string
      - `lt` string
    - `updated_at` object
      - `gte` string
      - `lte` string
      - `gt` string
      - `lt` string
    - `deleted_at` object: Default scope is { is_null: true } (live rows).
      - `gte` string
      - `lte` string
      - `is_null` boolean
  - `include` string[]: Relations to attach to every item; omit for the bare rows.
  - `sort` object: One sort column with its direction; default row_order asc, the grid row order (a per-team arrival sequence, never a job title).
    - `field` union, required
      - 'row_order' | 'created_at' | 'updated_at' | 'name'
      - string: Custom column of the pinned playtable, by slug.
    - `direction` 'asc' | 'desc': Default asc for row_order, desc otherwise.
  - `page_size` integer: 0..500, default 50; 0 = counts only, 1 = the first row.
  - `cursor` string, nullable: pagination.next_cursor of the previous page.
  - `filter_ast` unknown
  - `sids_only` boolean: Answer the matching sids only (the grid's ids-only model): items carry {sid} and nothing else.
  - `semantic` object: Semantic search over one embedded column (research §Filterable, the I5 engine); the result order is similarity.
    - `field` string, required: The semantic column, record.<slug>, whose value vectors are searched.
    - `query` string, required: Natural-language query embedded and matched against the column.
    - `k` integer: Nearest neighbours to keep before the filter applies. Default 100.
    - `min_score` number: The match floor on the score the rows come back with (0..1, cosine mapped as (1 + cos) / 2): a row below it is out, not last. Default: no floor, the k nearest stay.

## Response `200`

`search` success envelope.

- SearchCompaniesResponse
  - `success` true, required
  - `operation` 'search', required
  - `items` object[], required
    - `item` object, required
      - `sid` string, required
      - `team_sid` string, required
      - `playtable_sids` string[], required: Member tables (pt_tb_…) - MULTI membership; service-managed (create/import union, detach / table-delete pull); may be empty. Filtering/counting on it is ES-served.
      - `data_source_sid` string, nullable, required
      - `data_source_type` string, nullable, required
      - `data_source_item_sid` string, nullable, required
      - `ln_id` string, nullable, required: Numeric LinkedIn company id - strongest dedup key.
      - `ln_nickname` string, nullable, required: The company page vanity slug (acme-corp), lowercased; a page URL is reduced to it on write, never stored (SERVICE_CONVENTIONS §Y). Dedup key #2.
      - `context` object, nullable, required: DERIVED and read-only (CONTEXT_FIELDS_RESEARCH.md section 5): the name, the logo and the website a surface or a model shows next to the row, resolved at read from the observations ledger with the same precedence as a person's. The only place these words are: the row keeps identifiers (phase E, 2026-10-22).
        - `name` string, nullable, required
        - `picture_url` string, nullable, required: The logo: a permanent copy over a wire link; null = the reader falls back to the domain favicon.
        - `website` string, nullable, required: The full URL a source gave; domain on the row stays the identifier.
        - `sources` object, required: Per resolved key, where it came from and when.
      - `domain` string, nullable, required: Lowercased apex domain; derived from website when absent; dedup key #3.
      - `deal_size` number, nullable, required: Deal value attached to the account (decimal 15,2) - the one client-writable business column.
      - `status` 'ok' | 'failed_validation' | 'duplicate' | 'stoplisted', required: Service-managed (import validation, stoplist); not client-writable.
      - `status_reason` string, nullable, required: Human-readable context for the current status (validation error, matched stoplist identifier); service-written together with status. A duplicate carries none: its pointer is duplicate_of_sid.
      - `duplicate_of_sid` string, nullable, required: While status is duplicate: the PRIMARY account (cr_co_…) this one is a copy of - one primary per group, re-derived on every recheck, swapped by make_company_primary. Null otherwise.
      - `duplicate_matched_kind` 'ln_id' | 'ln_nickname' | 'domain', nullable, required: While status is duplicate: the identity column the late write collided on. Null otherwise.
      - `tag_sids` string[], nullable, required
      - `external_refs` union, required: Free-form CRM sync hook ({ hubspot_company_id: "902", … }); keys are integration-defined, stored as-is, round-tripped, never validated; PHP empty map serialises as [].
        - object
        - unknown[]
          - unknown
        - unknown
      - `last_enrich_at` string, nullable, required: ISO 8601 UTC; stamped when an enrichment cycle completes - drives enrichment scheduling.
      - `deleted_by` object, nullable, required
        - `actor_type` 'user' | 'support' | 'api_key' | 'system' | 'agent', required
        - `actor_sid` string, nullable, required
        - `team_sid` string, required
        - `actor_name` string, nullable: The OAuth client that acted ("Claude", "n8n"); null for a user, an API key or a system job.
        - `oauth_client_sid` string, nullable: The acting OAuth client (id_oc_*); null off the OAuth path.
        - `reason` string, nullable: Why a system actor wrote the row (snapshot_capture_job, ...); null otherwise.
        - `permissions` object, required: Internal audit context: the grant set the actor held at write time. Not a contract.
        - `cluster_id` integer, nullable: Internal audit context: the cluster that served the write.
        - `trace_id` string, nullable: Internal audit context: the trace id of the request that wrote the row.
      - `created_at` string, required
      - `updated_at` string, required
      - `deleted_at` string, nullable, required
      - `pipeline_stage_sid` string, nullable, required: The funnel stage the company is in (cr_ps_...): derived from its people unless pinned (pipeline_stages.md); null = no stage.
      - `pipeline_stage_source` 'derived' | 'manual', nullable, required: How the stage was set (CompanyPipelineStageSourceEnum): derived from the people, or pinned by a manual move; reset_pipeline_stages_derivation drops the pin. Null with no stage.
    - `included` object, required
      - `people_counts` object: PeopleCounts aggregate over the company's linked people (legacy leads-summary widget); FK auto-injected (people.company_sid = parent.sid), no other filters pass - see ./people.md.
        - `total_count` integer, required
      - `field_values` object[]: Trimmed cell projection (value, status, cost, finished_at - no meta.steps) across ALL member playtables; each row carries its playtable_sid. Order: playtable_sid, then field position. Full trace = playtable-field-values get - see ./playtable_field_values.md.
        - `sid` string, required
      - `last_activities` object[]: ActivityDomain rows (1:N, limit 20, occurred_at DESC) - see ./activities.md.
        - `sid` string, required
      - `notes` object[]: NoteDomain rows (1:N, limit 20, created_at DESC) - see ./notes.md.
        - `sid` string, required
      - `signals` object[]: The catalog names the account carries right now, evaluated at read time from the ledger: its own website visit (company_visited_our_site) and the account names over its people (ACCOUNT_PLAYS_RESEARCH.md §4.2: no_people, thin_coverage, nobody_reached, no_engagement, no_reply, exhausted, dormant, dm_missing), each with the instant its clock started and its age in days (both null for a stored fact with no clock). Empty when none holds. Read it before deciding where to top up or whom to re-engage.
        - `key` 'awaiting_reply' | 'opened_not_replied' | 'clicked_not_replied' | 'multi_opened' | 'accepted_not_replied' | 'note_read_not_replied' | 'reacted_not_replied' | 'invited_us' | 'followed_us' | 'lead_messaged_first' | 'unanswered_inbound' | 'draft_pending' | 'escalated_open' | 'task_overdue' | 'went_quiet' | 'reactivated' | 'invite_pending' | 'connection_removed' | 'blocked_us' | 'deleted_message' | 'bounced' | 'bounced_hard' | 'complained' | 'booking_link_sent' | 'objective_reached' | 'viewed_our_profile' | 'visited_our_site' | 'visited_not_replied' | 'visited_key_page' | 'multi_visited' | 'submitted_form' | 'company_visited_our_site' | 'no_people' | 'thin_coverage' | 'nobody_reached' | 'no_engagement' | 'no_reply' | 'exhausted' | 'dormant' | 'dm_missing', required: A catalog signal name. Thread and person names: awaiting_reply (our message is the last), opened_not_replied and clicked_not_replied (our email opened or its link clicked, no reply), multi_opened (3+ opens), accepted_not_replied (invite accepted, nothing after), note_read_not_replied (the accept delivered our invite note, no reply), reacted_not_replied (an emoji on our message, no reply), invited_us (their invitation to us, unanswered), followed_us, lead_messaged_first (an inbound thread), unanswered_inbound, draft_pending (an AI draft awaits approval), escalated_open (handed to a person, task open), task_overdue, went_quiet (they had replied, then nothing), reactivated (an inbound after 30+ quiet days), invite_pending (our invite out, neither accepted nor withdrawn), connection_removed, blocked_us, deleted_message, bounced, bounced_hard (the address is gone), complained, booking_link_sent, objective_reached (the live dialog landed its goal), viewed_our_profile. Website names: visited_our_site, visited_not_replied, visited_key_page (a page the team marked key), multi_visited, submitted_form, company_visited_our_site (someone from the company, nobody recognized; a person's and a company's). Account names, companies only (dormant also a person's): no_people (nobody to work), thin_coverage (under 3 people to work), nobody_reached, no_engagement (touched, nothing came back), no_reply (touched, no reply), exhausted (everyone nurture or negative), dormant (touched once, quiet since), dm_missing (a Wrong Decision Maker named, no positive person left). Tracking facts are private: never tell the lead (submitted_form excepted).
        - `since` string, nullable, required: ISO 8601 UTC: the instant the signal's clock started (our last message, the accept, the reaction...). null when the signal has no clock (draft_pending).
        - `age_days` integer, nullable, required: Whole days since `since`, at read time. null when the signal has no clock.
      - `freshness` object[]: The company page's read when a watching play keeps it fresh on the company (a companies-tab column reads it, one of the company's people is in the play's audience): the play, its cadence, the last read and the next. Empty when no switched-on daily play covers the company.
        - `object_sid` string, required: The row: cr_pp_ on a person, cr_co_ on a company.
        - `kind` 'profile' | 'company' | 'posts', required: profile: the LinkedIn profile (a people-tab column over the person). company: the company page (a people-tab column over a company URL on a person; a companies-tab column on a company). posts: the person's posts.
        - `read_at` string, nullable, required: ISO 8601 UTC: the row's last read of this kind, whoever read (the play, a workflow's Run column step, a column's own run, a hand run). null = never read.
        - `play_sid` string, required: pt_pl_ the play whose schedule keeps this kind fresh on the row: switched on, daily, its watch names the kind, a column of the row's tab reads the kind, and the row is in its audience (a company is when one of its people is). When several plays cover the row and the kind, the shortest cadence wins.
        - `play_name` string, required: The play's name, for "re-read 2 days ago by Keep data fresh".
        - `days` integer, required: The play's cadence for this kind, in days ("every 7 days").
        - `next_at` string, nullable, required: ISO 8601 UTC: read_at plus the cadence, when the schedule reads the row again. null when never read: the next run reads it.
  - `pagination` object, required
    - `next_cursor` string, nullable, required
    - `has_more` boolean, required
    - `total_count` integer, nullable, required
  - `applied_filters` object, required
  - `includes` string[], required
  - `meta` object, required
    - `trace_id` string, required: UUID v7; same 128-bit value as the X-Trace-Id header.
    - `span_id` string, required: 16 hex chars, root span of this request.
    - `timestamp` string, required: ISO 8601 UTC (Y-m-dTH:i:sZ), response time.
    - `duration_ms` integer, required: Server-side wall clock.
    - `team_sid` string, nullable: The team this call ran in (the token team, or the team_sid override). Null when unauthenticated; absent from pre-2026-08-20 backends.
    - `actor_type` string, nullable: user | agent | api_key | system. Null when unauthenticated; absent from pre-2026-08-20 backends.
  - `counts` object
    - `total_count` integer, required: Company rows matching the filter scope.
    - `with_linkedin_count` integer, required: Rows where ln_id IS NOT NULL OR ln_nickname IS NOT NULL - multi-column OR, no group axis covers it.
    - `with_domain_count` integer, required: Rows where domain IS NOT NULL - filterable but not groupable column.
    - `groups` object, required
      - `status` union, required: Rows per CompanyStatusEnum value (3 values, bounded).
        - object
        - unknown[]
          - unknown
      - `playtable_sids` union, required: Rows per member table (pt_tb_… keys); a company counts once per member table.
        - object
        - unknown[]
          - unknown
      - `data_source_sid` union, required: Rows per provenance source (ds_sc_… keys).
        - object
        - unknown[]
          - unknown
      - `signals` union, required: Rows per signal name the account carries at the catalog's default threshold, present names only (a filters aggregation, not a terms axis): how many accounts of the scope read no_engagement, exhausted, dm_missing and the rest, in one page_size:0 call (ACCOUNT_PLAYS_RESEARCH.md §4.4: the chips over the Companies grid, Creator's brief).
        - object
        - unknown[]
          - unknown

## Other responses

- `4XX`: MCP error envelope. `error.code` is one of validation_failed, nothing_to_update, not_found, relation_not_found, invalid_transition, limit_exceeded, payment_required, duplicate_rejected, conflict, delete_blocked, unauthorized, forbidden, rate_limited, not_implemented.
- `5XX`: MCP error envelope with `error.code` internal_error or service_unavailable.

---

[API](https://skmtc.dev/getsales/apis/getsales-public-contract-gs-service-playtables.md) - [All operations](https://skmtc.dev/getsales/apis/getsales-public-contract-gs-service-playtables/llms.txt) - [OpenAPI document](https://skmtc.dev/getsales/apis/getsales-public-contract-gs-service-playtables/revisions/aa394506515d?raw)
