---
title: "People metrics"
method: POST
path: "/api/people/metrics"
tags: ["people"]
---

# People metrics

`POST /api/people/metrics`

## Request

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

Composition/coverage aggregates over a filtered people scope (ES): email/linkedin/phone coverage rates and aggregated.counts as the scope stands NOW; added_count and deleted_count alone ride the period (created_at operators inside filter are silently ignored). Pass filter.playtable_sid for one table's stats, group_by for distributions (groups[] with counts + metrics per axis value). Messaging funnel metrics (sent/replied) live on activities, not here.

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

## Request body

- GetPeopleMetricsRequest: Request body of `get_people_metrics`.
  - `filter` object: Scopes the rows the metrics are computed over: the search filter operators. Omit for all rows.
    - `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
  - `period` object, required: Required. Single source of truth for the time window; period column = created_at.
    - `from` string, required: ISO 8601 UTC window start (inclusive).
    - `to` string, required: ISO 8601 UTC window end (exclusive); must be >= from.
  - `group_by` 'status' | 'linkedin_status' | 'email_status' | 'phone_status' | 'playtable_sid' | 'data_source_sid' | 'pipeline_stage_sid': Single axis from the groupable fields - adds groups[] with per-value counts + metrics. Cap 500 groups.

## Response `200`

`metrics` success envelope.

- GetPeopleMetricsResponse
  - `success` true, required
  - `operation` 'metrics', required
  - `metrics` object, required
    - `filter` union: Echo of the request PeopleSearchFilter (PHP empty map serialises as []).
      - object
      - unknown[]
        - unknown
    - `period` object, required
      - `from` string, required
      - `to` string, required
    - `aggregated` object, required
      - `counts` object, required
        - `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[]
              - …
          - `linkedin_status` union, required
            - object
            - unknown[]
              - …
          - `email_status` union, required
            - object
            - unknown[]
              - …
          - `phone_status` union, required
            - object
            - unknown[]
              - …
          - `playtable_sid` union, required
            - object
            - unknown[]
              - …
          - `data_source_sid` union, required
            - object
            - 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[]
              - …
      - `metrics` object, required
        - `added_count` integer, required: Rows with created_at within [period.from, period.to).
        - `deleted_count` integer, required: Rows soft-deleted within the period (counted with deleted scope).
        - `email_coverage_rate` number, nullable, required: with_email_count / total_count over the filter scope; null when total is 0.
        - `linkedin_coverage_rate` number, nullable, required: with_linkedin_count / total_count; null when total is 0.
        - `phone_coverage_rate` number, nullable, required: with_phone_count / total_count; null when total is 0.
    - `groups` object[]: Present only when group_by was set - one entry per axis value. Cap 500 groups.
      - `key` string, nullable, required: Value of the group_by axis (enum value / sid); null = rows without a value on the axis.
      - `counts` object, required
        - `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[]
              - …
          - `linkedin_status` union, required
            - object
            - unknown[]
              - …
          - `email_status` union, required
            - object
            - unknown[]
              - …
          - `phone_status` union, required
            - object
            - unknown[]
              - …
          - `playtable_sid` union, required
            - object
            - unknown[]
              - …
          - `data_source_sid` union, required
            - object
            - 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[]
              - …
      - `metrics` object, required
        - `added_count` integer, required: Rows with created_at within [period.from, period.to).
        - `deleted_count` integer, required: Rows soft-deleted within the period (counted with deleted scope).
        - `email_coverage_rate` number, nullable, required: with_email_count / total_count over the filter scope; null when total is 0.
        - `linkedin_coverage_rate` number, nullable, required: with_linkedin_count / total_count; null when total is 0.
        - `phone_coverage_rate` number, nullable, required: with_phone_count / total_count; null when total is 0.
  - `applied_filters` object
  - `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.

## 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)
