---
title: "Create person"
method: POST
path: "/api/people"
tags: ["people"]
---

# Create person

`POST /api/people`

## Request

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

Manually add a person to a playtable (single row; bulk ingestion is a playtable-run of type import). The dedup ladder is team-scoped, two tiers: identity keys (ln_member_id, raw ln_id / sn_id / recruiter_id, emails) and flag keys (ln_nickname, phones).

An identity-tier hit: without on_duplicate → 409 duplicate_rejected with context { existing_sid, already_in_playtable_sid, matched_identifier_kind }; on_duplicate:"skip" → the existing row, already_exists:true; "move" → re-homed into the target playtable. A flag-tier hit (nickname, phone) without on_duplicate CREATES the row flagged status=duplicate (duplicate_of_sid set); an explicit on_duplicate resolves it the same way as identity.

No identity field at all → status=no_contact_data (dedup skipped; enrichment can lift it to ok). A stoplist check runs at insert → status=stoplisted on match. A LinkedIn URL is parsed to its identifier axes at the edge; emails lowercase. full_name, avatar_url and company_name are manual observations of the birth (the item's context shows them), never columns.

When NOT: bulk import / bulk delete / moving batches (playtable-runs), profile content like position or location (playtable field values, not entity columns).

Contract:
- MCP tool `create_person`, registry package `mcp.playtables/people`, mount `playtables.people`.
- Operation `create`, response envelope `create`.
- Flags: none.

## Request body

- CreatePersonRequest: Request body of `create_person`.
  - `playtable_sid` string, required: Target table (pt_tb_…) - the person's single membership.
  - `full_name` string, nullable: Display name, 1..255: a manual observation (it outranks every source until reset), never a column; null = unnamed until a source names the row.
  - `avatar_url` string, uri, nullable: Profile photo URL (max 2048): a manual observation like full_name, never a column; null = no picture known.
  - `ln_member_id` string, nullable: LinkedIn member urn id - strongest dedup key.
  - `ln_id` string, nullable: Raw LinkedIn profile URN (ACoAA…); no spaces. Decodes to ln_member_id.
  - `sn_id` string, nullable: Raw Sales Navigator URN (ACwAA…); no spaces. Decodes to the same ln_member_id.
  - `recruiter_id` string, nullable: Raw Recruiter URN (AEMAA…); no spaces. Decodes to the same ln_member_id.
  - `ln_nickname` string, nullable: Vanity slug, or a profile URL: the service reduces a URL to its axis (a slug, or a URN moved to ln_id / sn_id / recruiter_id) and never stores the URL (SERVICE_CONVENTIONS §Y). Counts as contact data; dispatch still needs a URN.
  - `work_email` string, email, nullable: Stored lowercased - 3rd dedup key (with personal_email).
  - `personal_email` string, email, nullable: Personal address; stored lowercased, a dedup key together with work_email.
  - `work_phone_number` string, nullable: Plain string, E.164 preferred.
  - `personal_phone_number` string, nullable: Plain string, E.164 preferred; richer phone sets are field values.
  - `company_sid` string, nullable: Must exist; the person's company then resolves from the account (context.company).
  - `company_name` string, nullable: A name that is not an account, a manual observation; read only when company_sid is absent.
  - `tag_sids` string[], nullable: Each tag must exist (cr_tg_…).
  - `external_refs` object, nullable: Free-form CRM sync map; keys integration-defined, stored as-is.
  - `on_duplicate` 'skip' | 'move': skip = return the existing row; move = re-home it into playtable_sid. Applies on BOTH dedup tiers when given. Omitted: an identity-tier hit is 409, a flag-tier match (nickname, phone) is born flagged.

## Response `200`

`create` success envelope.

- CreatePersonResponse
  - `success` true, required
  - `operation` 'create', 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.
  - `already_exists` boolean, 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.

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