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

# Search people

`POST /api/people/search`

## Request

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

People: every table view is this tool with filter playtable_sid. Filter statuses, channel coverage (work_email is_null), tags, named signals [{key}]: clicked_not_replied, accepted_not_replied (accepted our invite, got nothing), not the activity feed. q is free text over name, company, emails, LinkedIn; profile content is cells (filter_ast); include field_values renders them. page_size 0 = counts, counts.groups.signals per signal. Fresh writes lag ES seconds: get_person reads your writes.

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

## Request body

- SearchPeopleRequest: Request body of `search_people`.
  - `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_pp_ sid(s).
      - `eq` string: Person sid (cr_pp_…).
      - `in` string[]
    - `playtable_sid` object: THE table context - every table view passes { eq: <table> }.
      - `eq` string
      - `in` string[]
    - `pipeline_stage_category` object: The stage's funnel bucket (one of seven), expanded on the server into the team's person stage sids of those categories: ask by name, never resolve stages. eq approaching = we reached out, nothing came back; in [engaging, positive] = something came back.
      - `eq` 'cold' | 'approaching' | 'engaging' | 'positive' | 'nurture' | 'negative' | 'disqualified'
      - `in` string[]
    - `company_sid` object
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `data_source_sid` object: Import provenance.
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `status` object
      - `eq` 'ok' | 'failed_validation' | 'no_contact_data' | 'duplicate' | 'stoplisted'
      - `ne` 'ok' | 'failed_validation' | 'no_contact_data' | '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: Person sid (cr_pp_…).
      - `in` string[]
      - `is_null` boolean
    - `linkedin_status` object
      - `eq` 'need_enrichment' | 'enrichment_failed' | 'no_data' | 'blocked' | 'ok'
      - `ne` 'need_enrichment' | 'enrichment_failed' | 'no_data' | 'blocked' | 'ok'
      - `in` string[]
      - `nin` string[]
    - `email_status` object
      - `eq` 'need_search' | 'no_data' | 'bounced' | 'ok' | 'catch_all' | 'unverified'
      - `ne` 'need_search' | 'no_data' | 'bounced' | 'ok' | 'catch_all' | 'unverified'
      - `in` string[]
      - `nin` string[]
      - `is_null` boolean
    - `phone_status` object
      - `eq` 'need_search' | 'no_data' | 'ok'
      - `ne` 'need_search' | 'no_data' | 'ok'
      - `in` string[]
      - `nin` string[]
      - `is_null` boolean
    - `tag_sids` object: Array-overlap semantics (any of) - in matches rows whose tag array overlaps the list; nin answers the complement, untagged rows included.
      - `in` string[]
      - `nin` string[]
    - `signals` object[]: ES-only. Named signals of the person, all must hold. The server expands a name into its keyword and clock range: never compose them. A bare entry takes the catalog default age, age_days moves it. A thread-only name is a 422 (ask search_conversations).
      - `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.
    - `ln_member_id` object: Exact identifier.
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `ln_id` object: Exact raw profile URN.
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `sn_id` object: Exact raw Sales Navigator URN.
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `ln_nickname` object: Exact vanity slug (lowercased); use q for fuzzy.
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `work_email` object: is_null:true = coverage gap.
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `personal_email` object
      - `eq` string
      - `in` string[]
      - `is_null` boolean
    - `q` string: Free text (ES people index) over the resolved name and company name, the emails and the slug. Filter a company by company_sid.
    - `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' | 'full_name' | 'company_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.

- SearchPeopleResponse
  - `success` true, required
  - `operation` 'search', required
  - `items` object[], required
    - `item` object, required
      - `sid` string, required
      - `team_sid` string, required
      - `playtable_sid` string, required: pt_tb_… - the SINGLE playtable membership; service-managed, changes only via move (create on_duplicate:move / run type move).
      - `data_source_sid` string, nullable, required
      - `data_source_item_sid` string, nullable, required
      - `company_sid` string, nullable, required
      - `context` object, nullable, required: DERIVED and read-only (CONTEXT_FIELDS_RESEARCH.md section 5): the name, first name, picture, role, headline and company a surface or a model shows next to the row, resolved at read from the observations ledger: a manual word until reset, then the newest LinkedIn-derived observation, then any other. The only place these words are: the row keeps identifiers (phase E, 2026-10-22). Null only on an event payload.
        - `name` string, nullable, required
        - `first_name` string, nullable, required: The source's own first name when it gave one, else the first word of name.
        - `picture_url` string, nullable, required: A permanent copy over a wire link, the wire link that lasts longest, an expired one counts as none.
        - `role` string, nullable, required: The title at the company (the Sales Navigator wire's position, the enrichment's).
        - `headline` string, nullable, required: The profile line (the regular and Recruiter wires, the inbox participant, the enrichment).
        - `company` object, nullable, required
          - `sid` string, nullable, required: cr_co_… when the person is linked to an account (the account's own resolved context follows); null = a name that is not an account, an import whose company column resolved to none.
          - `name` string, nullable, required
          - `domain` string, nullable, required
          - `picture_url` string, nullable, required
          - `ln_id` string, nullable, required: The company's LinkedIn id: the linked account's own, else the person's company id as a source or an edit gave it.
        - `sources` object, required: Per resolved key, where it came from and when.
      - `ln_member_id` string, nullable, required
      - `ln_id` string, nullable, required
      - `sn_id` string, nullable, required
      - `recruiter_id` string, nullable, required
      - `work_email` string, nullable, required
      - `personal_email` string, nullable, required
      - `work_phone_number` string, nullable, required
      - `personal_phone_number` string, nullable, required
      - `status` 'ok' | 'failed_validation' | 'no_contact_data' | 'duplicate' | 'stoplisted', required: Service-managed row validity (import validation, stoplist, duplicate detection) - not client-writable.
      - `duplicate_of_sid` string, nullable, required: While status is duplicate: the PRIMARY row (cr_pp_…) this one is a copy of - one primary per group, re-derived on every recheck, swapped by make_person_primary. Null otherwise.
      - `duplicate_matched_kind` 'ln_member_id' | 'ln_id' | 'sn_id' | 'recruiter_id' | 'email' | 'ln_nickname' | 'phone', nullable, required: While status is duplicate: the identifier the check matched on. Null otherwise.
      - `linkedin_status` 'need_enrichment' | 'enrichment_failed' | 'no_data' | 'blocked' | 'ok', required
      - `email_status` 'need_search' | 'no_data' | 'bounced' | 'ok' | 'catch_all' | 'unverified', nullable, required
      - `phone_status` 'need_search' | 'no_data' | 'ok', nullable, required
      - `tag_sids` string[], nullable, required
      - `external_refs` union, required: Free-form CRM sync map (hubspot_contact_id, …) - keys integration-defined, round-tripped as-is; PHP empty map serialises as [].
        - object
        - unknown[]
          - unknown
        - unknown
      - `last_enrich_at` string, nullable, required
      - `created_at` string, required
      - `updated_at` string, required
      - `deleted_at` string, nullable, required
      - `pipeline_stage_sid` string, nullable, required: The funnel stage the person is in (cr_ps_...), derived from activity or moved by hand (pipeline_stages.md); null = no stage.
    - `included` object, required
      - `company` object, nullable: CompanyDomain (N:1 via company_sid) - see ./companies.md.
        - `sid` string, required
      - `field_values` object[]: PlaytableFieldValueDomain rows - ALL live cells of the person's playtable (the table-row hydration: renders the custom columns), ordered by field position - see ./playtable_field_values.md.
        - `sid` string, required
      - `workflow_records` object[]: WorkflowRecordDomain rows in a NON-TERMINAL status only (active memberships; 1:N, limit 20, created_at DESC) - see ./workflow_records.md.
        - `sid` string, required
      - `last_activities` object[]: ActivityDomain rows (1:N, limit 20, occurred_at DESC) - the ingested timeline - see ./activities.md.
        - `sid` string, required
      - `notes` object[]: NoteDomain rows (1:N, limit 20, created_at DESC) - see ./notes.md.
        - `sid` string, required
      - `engagement` object: The person's engagement counters and dates folded over EVERY fact of the activities ledger (the search document's `engagement` keys, D43: messages sent and received per channel, opens, clicks, invitations sent / accepted / withdrawn, visits, likes, comments, endorsements, bounces, unsubscribes, replies, touches, the last touch and last message with their channel and direction). Every key present: 0 / null until a fact moves it. What the LinkedIn channel and Email channel widget cells draw; shares one read with engagement_by_sender.
      - `engagement_by_sender` object[]: The same counters per sender profile (ub_sp_ sid): the facts whose payload names it. Empty when no fact of the person names a sender profile.
        - `sender_profile_sid` string, required
      - `signals` object[]: The catalog names the person carries right now (SIGNALS_RESEARCH.md §5.3: evaluated from the engagement counters across every sender profile), each with the instant its clock started and its age in days. Empty when none holds. Ask it before drafting or deciding a next step; the thread-level names (drafts, escalations, tasks, reactions) are get_conversation's.
        - `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[]: Per read kind a watching play keeps fresh on the person (the LinkedIn profile, the company page a people-tab column reads, the posts): the play, its cadence, the last read and the next. One entry per kind at most; empty when no switched-on daily play covers the person. How old a profile read is, and whether the table re-reads it on its own.
        - `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: People rows matching the filter scope.
    - `with_email_count` integer, required: work_email IS NOT NULL OR personal_email IS NOT NULL (multi-column OR).
    - `with_phone_count` integer, required: work_phone_number IS NOT NULL OR personal_phone_number IS NOT NULL (multi-column OR).
    - `with_linkedin_count` integer, required: Any LinkedIn identity axis present: ln_member_id, ln_id, sn_id, recruiter_id or ln_nickname (multi-column OR).
    - `with_company_count` integer, required: company_sid IS NOT NULL (filterable but not groupable column).
    - `groups` object, required
      - `status` union, required
        - object
        - unknown[]
          - unknown
      - `linkedin_status` union, required
        - object
        - unknown[]
          - unknown
      - `email_status` union, required
        - object
        - unknown[]
          - unknown
      - `phone_status` union, required
        - object
        - unknown[]
          - unknown
      - `playtable_sid` union, required
        - object
        - unknown[]
          - unknown
      - `data_source_sid` union, required
        - object
        - unknown[]
          - unknown
      - `signals` union, required: Rows per signal name the person carries at the catalog's default threshold, present names only (a filters aggregation, not a terms axis): how many people of the scope read awaiting_reply, went_quiet, dormant and the rest, in one page_size:0 call (ACCOUNT_PLAYS_RESEARCH.md §4.4: the chips over the People grid).
        - 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)
