> ## Documentation Index
> Fetch the complete documentation index at: https://modem.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Link external records to people and companies

> Idempotently link external platform records (e.g. Stripe customers) to Modem people and companies.

Each `person` / `company` block states the facts the platform knows about that side of the record — not an assertion of how to resolve it. Modem owns all matching.

**Person** — matched by platform `identity` first, then by exact `email`. A bare email matches an existing person but never creates one (it may be a shared inbox like billing@); creation requires `displayName`.

**Company** — matched by the first hit, in trust order:

1. an existing platform `identity` link for this block
2. the resolved person's current company
3. stated `domains`, then the domain derived from `person.email` (free-mail domains such as gmail.com carry no signal)
4. an exact, case-insensitive `name` match
5. otherwise created — only when `name` is stated

When both a person and a company resolve, their membership is recorded.

If the platform identity and a natural key (email/domain) resolve to different entities, the identity match wins and nothing is merged; the other entity is returned as `conflictWith` — a probable duplicate to reconcile deliberately.

Removing a stored link is not part of this endpoint; it will ship separately.



## OpenAPI

````yaml /api-reference/openapi.json post /identify
openapi: 3.1.1
info:
  description: >-
    The public Modem API. Authenticate with an organization API key:
    `Authorization: Bearer modem_…`.
  title: Modem API
  version: 1.0.0
servers:
  - description: Production
    url: https://api.modem.dev/v1
security:
  - bearerAuth: []
paths:
  /identify:
    post:
      tags:
        - Identify
      summary: Link external records to people and companies
      description: >-
        Idempotently link external platform records (e.g. Stripe customers) to
        Modem people and companies.


        Each `person` / `company` block states the facts the platform knows
        about that side of the record — not an assertion of how to resolve it.
        Modem owns all matching.


        **Person** — matched by platform `identity` first, then by exact
        `email`. A bare email matches an existing person but never creates one
        (it may be a shared inbox like billing@); creation requires
        `displayName`.


        **Company** — matched by the first hit, in trust order:


        1. an existing platform `identity` link for this block

        2. the resolved person's current company

        3. stated `domains`, then the domain derived from `person.email`
        (free-mail domains such as gmail.com carry no signal)

        4. an exact, case-insensitive `name` match

        5. otherwise created — only when `name` is stated


        When both a person and a company resolve, their membership is recorded.


        If the platform identity and a natural key (email/domain) resolve to
        different entities, the identity match wins and nothing is merged; the
        other entity is returned as `conflictWith` — a probable duplicate to
        reconcile deliberately.


        Removing a stored link is not part of this endpoint; it will ship
        separately.
      operationId: identities.identify
      requestBody:
        content:
          application/json:
            schema:
              properties:
                identifications:
                  items:
                    properties:
                      company:
                        properties:
                          description:
                            minLength: 1
                            type: string
                          domains:
                            description: >-
                              Natural keys. Matches (or create-seeds) a company
                              by domain. A free-mail domain never matches,
                              creates, or is stamped (no signal), but does not
                              suppress name-keyed creation.
                            items:
                              minLength: 1
                              type: string
                            maxItems: 10
                            type: array
                          identity:
                            description: >-
                              Platform pointer. Matched identity-first; upserted
                              onto the matched/created company.
                            properties:
                              platform:
                                description: >-
                                  Source platform slug (e.g. "stripe",
                                  "salesforce").
                                pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
                                type: string
                              platformId:
                                description: >-
                                  The record id on the platform (e.g. a Stripe
                                  customer id like "cus_…").
                                minLength: 1
                                type: string
                            required:
                              - platform
                              - platformId
                            type: object
                          logoUrl:
                            format: uri
                            type: string
                          name:
                            description: Enables creation when no match is found.
                            minLength: 1
                            type: string
                          overwrite:
                            description: >-
                              Scalar fields to overwrite even when already
                              populated. Default is set-once.
                            items:
                              enum:
                                - description
                                - logoUrl
                                - name
                              type: string
                            type: array
                        type: object
                      person:
                        properties:
                          avatarUrl:
                            format: uri
                            type: string
                          description:
                            minLength: 1
                            type: string
                          displayName:
                            description: Enables creation when no match is found.
                            minLength: 1
                            type: string
                          email:
                            description: >-
                              Natural key. Matches a person by exact, lowercased
                              email. Never creates on its own.
                            format: email
                            type: string
                          identity:
                            description: >-
                              Platform pointer. Matched identity-first; upserted
                              onto the matched/created person.
                            properties:
                              platform:
                                description: >-
                                  Source platform slug (e.g. "stripe",
                                  "salesforce").
                                pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
                                type: string
                              platformId:
                                description: >-
                                  The record id on the platform (e.g. a Stripe
                                  customer id like "cus_…").
                                minLength: 1
                                type: string
                            required:
                              - platform
                              - platformId
                            type: object
                          overwrite:
                            description: >-
                              Scalar fields to overwrite even when already
                              populated. Default is set-once.
                            items:
                              enum:
                                - avatarUrl
                                - description
                                - displayName
                              type: string
                            type: array
                        type: object
                    type: object
                  maxItems: 50
                  minItems: 1
                  type: array
              required:
                - identifications
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  results:
                    items:
                      properties:
                        company:
                          anyOf:
                            - properties:
                                conflictWith:
                                  anyOf:
                                    - type: string
                                    - type: 'null'
                                  description: >-
                                    A probable duplicate surfaced as a merge
                                    candidate: the platform identity resolved to
                                    `id`, but the natural key (email/domain) is
                                    owned by this different existing entity.
                                    identify never merges — it's left for a
                                    deliberate merge. Null when there was no
                                    conflict.
                                created:
                                  type: boolean
                                id:
                                  type: string
                              required:
                                - conflictWith
                                - created
                                - id
                              type: object
                            - type: 'null'
                        membershipLinked:
                          type: boolean
                        person:
                          anyOf:
                            - properties:
                                conflictWith:
                                  anyOf:
                                    - type: string
                                    - type: 'null'
                                  description: >-
                                    A probable duplicate surfaced as a merge
                                    candidate: the platform identity resolved to
                                    `id`, but the natural key (email/domain) is
                                    owned by this different existing entity.
                                    identify never merges — it's left for a
                                    deliberate merge. Null when there was no
                                    conflict.
                                created:
                                  type: boolean
                                id:
                                  type: string
                              required:
                                - conflictWith
                                - created
                                - id
                              type: object
                            - type: 'null'
                      required:
                        - company
                        - membershipLinked
                        - person
                      type: object
                    type: array
                required:
                  - results
                type: object
          description: OK
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Invalid request (validation failed).
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Missing or invalid API key.
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: The API key does not have access to this resource.
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: The requested resource was not found.
        '429':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Rate limit exceeded.
        '500':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Internal server error.
components:
  schemas:
    Error:
      properties:
        code:
          description: Machine-readable error code, e.g. `NOT_FOUND`.
          examples:
            - NOT_FOUND
          type: string
        data:
          description: Optional structured error details.
        defined:
          description: True if the error was declared in the procedure contract.
          type: boolean
        message:
          description: Human-readable error message.
          type: string
        status:
          description: HTTP status code.
          examples:
            - 404
          type: integer
      required:
        - code
        - defined
        - message
        - status
      type: object
  securitySchemes:
    bearerAuth:
      description: Organization API key (`modem_…`).
      scheme: bearer
      type: http

````