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

# Create or update a contact

> The endpoint to build a sync on. Matches an existing contact by `phone`; creates one when there is no match.

Returns **201** when a contact was created and **200** when an existing one was matched. The update half is a patch: upsert never clears a field you did not send.

Safe to run concurrently — two simultaneous upserts with the same phone produce one contact, not a duplicate.



## OpenAPI

````yaml /api-reference/openapi.json put /v1/contacts
openapi: 3.1.0
info:
  description: >-
    The Sailer REST API for CRM, conversations, and campaigns.


    **Authentication.** Send a scoped token as `Authorization: Bearer
    sk_live_...`.

    Tokens are workspace-scoped; the workspace is implied by the token, so no

    tenant header is needed. During early access, tokens are issued by Sailer on

    request — contact your Sailer representative.


    **Pagination.** Collections return `{data, meta, links}`. Follow
    `links.next`

    until it is `null`; treat the cursor as opaque.


    **Errors.** Every non-2xx response is `{"error": {...}}` with a stable
    `code`.

    Quote `error.request_id` when contacting support.


    **Custom fields.** Workspace-defined fields appear under `custom_fields`,
    keyed

    by `field_key`. Read a record to see which keys a workspace uses; a
    dedicated

    field-discovery endpoint is not part of this release.
  title: Sailer API
  version: 1.0.0
servers:
  - url: https://api.chatsailer.com
security:
  - SailerApiToken: []
tags:
  - description: Token introspection and workspace capabilities.
    name: Meta
  - description: >-
      People you talk to. Called *leads* in older Sailer surfaces; `contact` is
      the current name.
    name: Contacts
  - description: Companies that contacts belong to (B2B accounts).
    name: Organizations
  - description: Opportunities moving through a pipeline.
    name: Deals
  - description: Pipelines and their stages.
    name: Pipelines
  - description: Field definitions, including custom fields.
    name: Fields
  - description: Free-text annotations on CRM records.
    name: Notes
  - description: Scheduled and logged CRM activities.
    name: Activities
  - description: Per-entity label catalogs.
    name: Tags
  - description: Message threads with contacts, across every channel.
    name: Conversations
  - description: Outbound campaigns and their participants.
    name: Campaigns
paths:
  /v1/contacts:
    put:
      tags:
        - Contacts
      summary: Create or update a contact
      description: >-
        The endpoint to build a sync on. Matches an existing contact by `phone`;
        creates one when there is no match.


        Returns **201** when a contact was created and **200** when an existing
        one was matched. The update half is a patch: upsert never clears a field
        you did not send.


        Safe to run concurrently — two simultaneous upserts with the same phone
        produce one contact, not a duplicate.
      operationId: upsert_contact
      parameters:
        - description: >-
            Comma-separated relationships to inline in the response. Unexpanded
            relations are still identified by their `*_id` field.
          in: query
          name: expand
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            description: >-
              Comma-separated relationships to inline in the response.
              Unexpanded relations are still identified by their `*_id` field.
            examples:
              - organization,owner
            title: Expand
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactUpsert'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
          description: Successful Response
        '201':
          description: A new contact was created.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      security:
        - SailerApiToken: []
components:
  schemas:
    ContactUpsert:
      additionalProperties: false
      description: >-
        Body for `PUT /v1/contacts`.


        Matched on `phone`, which is unique per workspace and canonicalized
        before

        comparison, so `(11) 98765-4321` and `+55 11 98765-4321` are the same
        key.
      properties:
        address:
          anyOf:
            - type: string
            - type: 'null'
          title: Address
        city:
          anyOf:
            - type: string
            - type: 'null'
          title: City
        company_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Company Name
        custom_fields:
          additionalProperties: true
          title: Custom Fields
          type: object
        email:
          anyOf:
            - type: string
            - type: 'null'
          title: Email
        first_name:
          description: Required; a nameless contact is not useful.
          minLength: 1
          title: First Name
          type: string
        job_title:
          anyOf:
            - type: string
            - type: 'null'
          title: Job Title
        last_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Last Name
        on_match:
          $ref: '#/components/schemas/OnMatch'
          default: update
          description: What to do when a contact with this phone already exists.
        organization_id:
          anyOf:
            - description: Unique identifier for an organization.
              examples:
                - org_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4
              pattern: ^org_[0-9a-f]{32}$
              type: string
            - type: 'null'
          title: Organization Id
        owner_id:
          anyOf:
            - description: Unique identifier for an user.
              examples:
                - usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4
              pattern: ^usr_[0-9a-f]{32}$
              type: string
            - type: 'null'
          title: Owner Id
        phone:
          description: Required. Any format; normalized to E.164.
          title: Phone
          type: string
        state:
          anyOf:
            - type: string
            - type: 'null'
          title: State
        visibility:
          $ref: '#/components/schemas/RecordVisibility'
          default: company
      required:
        - first_name
        - phone
      title: ContactUpsert
      type: object
    Contact:
      description: A person in your CRM.
      properties:
        address:
          anyOf:
            - type: string
            - type: 'null'
          title: Address
        avatar_url:
          anyOf:
            - type: string
            - type: 'null'
          description: Short-lived signed URL; do not store it.
          title: Avatar Url
        channels:
          items:
            $ref: '#/components/schemas/ContactChannel'
          title: Channels
          type: array
        city:
          anyOf:
            - type: string
            - type: 'null'
          title: City
        company_name:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Free-text employer name. Unrelated to your workspace and to
            `organization`, which is a CRM record.
          title: Company Name
        created_at:
          format: date-time
          title: Created At
          type: string
        custom_fields:
          additionalProperties: true
          description: >-
            Every custom field defined on contacts in this workspace, keyed by
            field key and typed from its definition. Unset fields are `null`, so
            reading any contact shows the full set of keys in use.
          title: Custom Fields
          type: object
        email:
          anyOf:
            - type: string
            - type: 'null'
          title: Email
        first_name:
          anyOf:
            - type: string
            - type: 'null'
          title: First Name
        id:
          description: Unique identifier for a contact.
          examples:
            - con_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4
          pattern: ^con_[0-9a-f]{32}$
          title: Id
          type: string
        job_title:
          anyOf:
            - type: string
            - type: 'null'
          title: Job Title
        last_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Last Name
        lost_reason:
          anyOf:
            - type: string
            - type: 'null'
          description: Name of the reason, when `status` is `lost`.
          title: Lost Reason
        name:
          anyOf:
            - type: string
            - type: 'null'
          description: Display name. Derived from `first_name` and `last_name`.
          title: Name
        object:
          const: contact
          default: contact
          title: Object
          type: string
        organization:
          anyOf:
            - $ref: '#/components/schemas/Organization'
            - type: 'null'
          description: Populated only when `organization` is in `expand`.
        organization_id:
          anyOf:
            - description: Unique identifier for an organization.
              examples:
                - org_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4
              pattern: ^org_[0-9a-f]{32}$
              type: string
            - type: 'null'
          title: Organization Id
        owner:
          anyOf:
            - $ref: '#/components/schemas/User'
            - type: 'null'
          description: Populated only when `owner` is in `expand`.
        owner_id:
          anyOf:
            - description: Unique identifier for an user.
              examples:
                - usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4
              pattern: ^usr_[0-9a-f]{32}$
              type: string
            - type: 'null'
          title: Owner Id
        phone:
          anyOf:
            - type: string
            - type: 'null'
          description: E.164. Normalized on write.
          title: Phone
        score:
          anyOf:
            - $ref: '#/components/schemas/LeadScore'
            - type: 'null'
        state:
          anyOf:
            - type: string
            - type: 'null'
          title: State
        status:
          $ref: '#/components/schemas/LeadStatus'
          description: Where the contact stands.
        tags:
          items:
            $ref: '#/components/schemas/Tag'
          title: Tags
          type: array
        updated_at:
          format: date-time
          title: Updated At
          type: string
        visibility:
          $ref: '#/components/schemas/RecordVisibility'
          description: >-
            Governs who sees this record in the Sailer UI. API tokens are
            workspace-scoped and are not filtered by it.
      required:
        - id
        - status
        - visibility
        - created_at
        - updated_at
      title: Contact
      type: object
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          title: Detail
          type: array
      title: HTTPValidationError
      type: object
    OnMatch:
      description: What to do when an upsert matches an existing contact.
      enum:
        - update
        - ignore
      title: OnMatch
      type: string
    RecordVisibility:
      description: >-
        CRM record visibility levels; resolved live against the owner's current
        teams.
      enum:
        - private
        - team
        - team_and_subteams
        - company
      title: RecordVisibility
      type: string
    ContactChannel:
      description: >-
        Where a contact is reachable.


        An array rather than a ``whatsapp_*`` column cluster: Instagram and
        Telegram

        arrive without a breaking change, and an empty list is a meaningful "not

        reachable anywhere" signal.
      properties:
        identifier:
          description: Address on that platform.
          title: Identifier
          type: string
        platform:
          description: Channel platform, e.g. `whatsapp`.
          title: Platform
          type: string
        status:
          anyOf:
            - type: string
            - type: 'null'
          description: Reachability, when the platform reports it.
          title: Status
        username:
          anyOf:
            - type: string
            - type: 'null'
          title: Username
      required:
        - platform
        - identifier
      title: ContactChannel
      type: object
    Organization:
      description: A company a contact belongs to.
      properties:
        created_at:
          format: date-time
          title: Created At
          type: string
        id:
          description: Unique identifier for an organization.
          examples:
            - org_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4
          pattern: ^org_[0-9a-f]{32}$
          title: Id
          type: string
        name:
          title: Name
          type: string
        object:
          const: organization
          default: organization
          title: Object
          type: string
        owner:
          anyOf:
            - $ref: '#/components/schemas/User'
            - type: 'null'
          description: Populated only when `owner` is included in `expand`.
        owner_id:
          anyOf:
            - description: Unique identifier for an user.
              examples:
                - usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4
              pattern: ^usr_[0-9a-f]{32}$
              type: string
            - type: 'null'
          description: Teammate responsible for this organization.
          title: Owner Id
        tags:
          items:
            $ref: '#/components/schemas/Tag'
          title: Tags
          type: array
        updated_at:
          format: date-time
          title: Updated At
          type: string
        visibility:
          $ref: '#/components/schemas/RecordVisibility'
          description: >-
            Governs who sees this record in the Sailer UI. API tokens are
            workspace-scoped and are not filtered by it.
        website:
          anyOf:
            - type: string
            - type: 'null'
          title: Website
      required:
        - id
        - name
        - visibility
        - created_at
        - updated_at
      title: Organization
      type: object
    User:
      description: A Sailer teammate. Exposed only as the owner of a record.
      properties:
        email:
          anyOf:
            - type: string
            - type: 'null'
          title: Email
        id:
          description: Unique identifier for an user.
          examples:
            - usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4
          pattern: ^usr_[0-9a-f]{32}$
          title: Id
          type: string
        name:
          anyOf:
            - type: string
            - type: 'null'
          title: Name
        object:
          const: user
          default: user
          title: Object
          type: string
      required:
        - id
      title: User
      type: object
    LeadScore:
      enum:
        - cold
        - warm
        - hot
      title: LeadScore
      type: string
    LeadStatus:
      enum:
        - open
        - on_hold
        - won
        - lost
      title: LeadStatus
      type: string
    Tag:
      description: A CRM label. The catalogue is per entity type.
      properties:
        color:
          anyOf:
            - type: string
            - type: 'null'
          description: Hex colour used by the Sailer UI, e.g. `#FFAA00`.
          title: Color
        id:
          description: Unique identifier for a tag.
          examples:
            - tag_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4
          pattern: ^tag_[0-9a-f]{32}$
          title: Id
          type: string
        name:
          title: Name
          type: string
        object:
          const: tag
          default: tag
          title: Object
          type: string
      required:
        - id
        - name
      title: Tag
      type: object
    ValidationError:
      properties:
        ctx:
          title: Context
          type: object
        input:
          title: Input
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          title: Location
          type: array
        msg:
          title: Message
          type: string
        type:
          title: Error Type
          type: string
      required:
        - loc
        - msg
        - type
      title: ValidationError
      type: object
  securitySchemes:
    SailerApiToken:
      description: >-
        A workspace API token. Create one in Settings → API. Send it as
        `Authorization: Bearer sk_live_...`.
      scheme: bearer
      type: http

````